Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtDistGraphiteStats
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 4
156
0.00% covered (danger)
0.00%
0 / 1
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPopularList
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
56
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearCache
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\ExtensionDistributor\Stats;
4
5use MediaWiki\Extension\ExtensionDistributor\Providers\ExtDistProvider;
6use MediaWiki\Json\FormatJson;
7use MediaWiki\MediaWikiServices;
8use Psr\Log\LoggerAwareInterface;
9use Psr\Log\LoggerInterface;
10use Wikimedia\ObjectCache\BagOStuff;
11
12/**
13 * Class for retrieving stats about downloads from a Graphite render url
14 *
15 * @author Addshore
16 */
17class ExtDistGraphiteStats implements LoggerAwareInterface {
18
19    /**
20     * @var LoggerInterface
21     */
22    private $logger;
23
24    /**
25     * Sets a logger instance on the object
26     */
27    public function setLogger( LoggerInterface $logger ): void {
28        $this->logger = $logger;
29    }
30
31    /**
32     * @param string $type 'extensions' or 'skins'
33     *
34     * TODO we need some way to limit the number of extensions returning?
35     *
36     * @return array|bool array of extensions in order of popularity or false on failure
37     */
38    public function getPopularList( $type ) {
39        global $wgExtDistGraphiteRenderApi, $wgServerName, $wgStatsdMetricPrefix;
40        if ( !$wgExtDistGraphiteRenderApi ) {
41            return false;
42        }
43
44        $objectcachefactory = MediaWikiServices::getInstance()->getObjectCacheFactory();
45
46        $cache = $objectcachefactory->getInstance( CACHE_ANYTHING );
47        $cacheKey = $this->getCacheKey( $cache, $type );
48
49        $cachedValue = $cache->get( $cacheKey );
50        if ( $cachedValue ) {
51            $this->logger->debug( "Retrieved PopularList of $type from cache" );
52            // @phan-suppress-next-line PhanCoalescingNeverNull $cachedValue can be null
53            return $cachedValue ?? false;
54        }
55
56        $metric = "$wgStatsdMetricPrefix.extdist.$type.*.*.sum";
57        $requestParams = [
58            'target' => 'sortByMaxima(groupByNode(summarize(' . $metric . ',"4w","sum",true),3,"sum"))',
59            'format' => 'json',
60            'from' => '-4w',
61            'until' => 'now',
62        ];
63
64        $httpOptions = [
65            'userAgent' => "$wgServerName - ExtensionDistributor  - MediaWiki Extension",
66        ];
67        $url = $wgExtDistGraphiteRenderApi . '/?' . http_build_query( $requestParams );
68        $req = MediaWikiServices::getInstance()->getHttpRequestFactory()
69            ->create( $url, $httpOptions, __METHOD__ );
70        $status = $req->execute();
71        if ( !$status->isOK() ) {
72            $this->logger->error( "Could not fetch popularList of $type from graphite, " .
73                "received: {$status}"
74            );
75            // Store a negative cache entry so we don't hammer graphite
76            $cache->set( $cacheKey, null, 60 * 60 );
77            return false;
78        }
79
80        $info = wfObjectToArray( FormatJson::decode( $req->getContent(), true ), true );
81        '@phan-var array[] $info';
82
83        $popularList = [];
84        foreach ( $info as $dataSet ) {
85            $popularList[] = $dataSet['target'];
86        }
87        if ( !$popularList ) {
88            $this->logger->error( "Graphite result resulted in empty PopularList of $type" );
89            // Store a negative cache entry so we don't hammer graphite
90            $cache->set( $cacheKey, null, 60 * 60 );
91            return false;
92        }
93
94        $popularList = array_slice( $popularList, 0, 15 );
95
96        // Cache list for 1 day
97        $cacheSuccess = $cache->set( $cacheKey, $popularList, 60 * 60 * 24 );
98        if ( !$cacheSuccess ) {
99            $this->logger->error( "Could not store PopularList of $type in cache." );
100        }
101
102        return $popularList;
103    }
104
105    /**
106     * @param BagOStuff $cache
107     * @param string $type
108     * @return string
109     */
110    private function getCacheKey( BagOStuff $cache, $type ) {
111        return $cache->makeKey( 'extdist', 'GraphiteStats', $type, 'PopularList' );
112    }
113
114    public function clearCache() {
115        $cache = MediaWikiServices::getInstance()
116            ->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
117        $typesToClear = [
118            ExtDistProvider::EXTENSIONS,
119            ExtDistProvider::SKINS
120        ];
121        foreach ( $typesToClear as $type ) {
122            $success = $cache->delete( $this->getCacheKey( $cache, $type ) );
123            if ( !$success ) {
124                $this->logger->error( "Failed to clear PopularList cache for $type" );
125            }
126        }
127    }
128
129}