Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.27% covered (warning)
67.27%
37 / 55
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslatorActivity
67.27% covered (warning)
67.27%
37 / 55
63.64% covered (warning)
63.64%
7 / 11
36.46
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 inLanguage
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 getFromCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCacheKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStale
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 queueCacheRefresh
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doQueryAndCache
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
2.01
 addToCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 updateAllLanguages
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 updateLanguage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isValidLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use BagOStuff;
7use InvalidArgumentException;
8use JobQueueGroup;
9use MediaWiki\Extension\Translate\Utilities\Utilities;
10use MediaWiki\Languages\LanguageNameUtils;
11use PoolCounterWorkViaCallback;
12use Wikimedia\Timestamp\ConvertibleTimestamp;
13
14/**
15 * Handles caching of translator activity.
16 *
17 * @author Niklas Laxström
18 * @license GPL-2.0-or-later
19 * @since 2020.04
20 */
21class TranslatorActivity {
22    public const CACHE_TIME = 3 * 24 * 3600;
23    // 25 hours so that it's easy to configure the maintenance script run daily
24    public const CACHE_STALE = 25 * 3600;
25    private BagOStuff $cache;
26    private TranslatorActivityQuery $query;
27    private JobQueueGroup $jobQueue;
28
29    public function __construct(
30        BagOStuff $cache,
31        TranslatorActivityQuery $query,
32        JobQueueGroup $jobQueue
33    ) {
34        $this->cache = $cache;
35        $this->query = $query;
36        $this->jobQueue = $jobQueue;
37    }
38
39    /**
40     * Get translations activity for a given language.
41     * @throws StatisticsUnavailable If loading statistics is temporarily not possible.
42     */
43    public function inLanguage( string $language ): array {
44        if ( !$this->isValidLanguage( $language ) ) {
45            throw new InvalidArgumentException( "Invalid language tag '$language'" );
46        }
47
48        $cachedValue = $this->getFromCache( $language );
49
50        if ( is_array( $cachedValue ) ) {
51            if ( $this->isStale( $cachedValue ) ) {
52                $this->queueCacheRefresh( $language );
53            }
54
55            return $cachedValue;
56        }
57
58        $queriedValue = $this->doQueryAndCache( $language );
59        if ( !$queriedValue ) {
60            throw new StatisticsUnavailable( "Unable to load stats" );
61        }
62
63        return $queriedValue;
64    }
65
66    private function getFromCache( string $language ) {
67        $cacheKey = $this->getCacheKey( $language );
68        return $this->cache->get( $cacheKey );
69    }
70
71    private function getCacheKey( string $language ): string {
72        return $this->cache->makeKey( 'translate-translator-activity-v4', $language );
73    }
74
75    private function isStale( array $value ): bool {
76        $age = intval( ConvertibleTimestamp::now( TS_UNIX ) ) - $value['asOfTime'];
77        return $age >= self::CACHE_STALE;
78    }
79
80    private function queueCacheRefresh( string $language ): void {
81        $job = UpdateTranslatorActivityJob::newJobForLanguage( $language );
82        $this->jobQueue->push( $job );
83    }
84
85    private function doQueryAndCache( string $language ) {
86        $now = (int)ConvertibleTimestamp::now( TS_UNIX );
87
88        $work = new PoolCounterWorkViaCallback(
89            'TranslateFetchTranslators', "TranslateFetchTranslators-$language", [
90                'doWork' => function () use ( $language, $now ) {
91                    $users = $this->query->inLanguage( $language );
92                    $data = [ 'users' => $users, 'asOfTime' => $now ];
93                    $this->addToCache( $data, $language );
94                    return $data;
95                },
96                'doCachedWork' => function () use ( $language ) {
97                    $data = $this->getFromCache( $language );
98                    // Use new cache value from other thread
99                    return is_array( $data ) ? $data : false;
100                },
101            ]
102        );
103
104        return $work->execute();
105    }
106
107    private function addToCache( array $value, string $language ): void {
108        $cacheKey = $this->getCacheKey( $language );
109        $this->cache->set( $cacheKey, $value, self::CACHE_TIME );
110    }
111
112    /** Update cache for all languages, even if not stale. */
113    public function updateAllLanguages(): void {
114        $now = (int)ConvertibleTimestamp::now( TS_UNIX );
115
116        $data = $this->query->inAllLanguages();
117        // In case there is no activity for a supported languages, cache empty results
118        $validLanguages = Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS );
119        foreach ( $validLanguages as $language ) {
120            $data[$language] ??= [];
121        }
122
123        foreach ( $data as $language => $users ) {
124            if ( !$this->isValidLanguage( $language ) ) {
125                continue;
126            }
127
128            $data = [ 'users' => $users, 'asOfTime' => $now ];
129            $this->addToCache( $data, $language );
130        }
131    }
132
133    /**
134     * Update cache for one language, even if not stale.
135     * @throws StatisticsUnavailable If loading statistics is temporarily not possible.
136     */
137    public function updateLanguage( string $language ): void {
138        if ( !$this->isValidLanguage( $language ) ) {
139            throw new InvalidArgumentException( "Invalid language tag '$language'" );
140        }
141
142        $queriedValue = $this->doQueryAndCache( $language );
143        if ( !$queriedValue ) {
144            throw new StatisticsUnavailable( 'Unable to load stats' );
145        }
146    }
147
148    private function isValidLanguage( string $language ): bool {
149        return Utilities::isSupportedLanguageCode( $language );
150    }
151}