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     * @param LoggerInterface $logger
28     */
29    public function setLogger( LoggerInterface $logger ) {
30        $this->logger = $logger;
31    }
32
33    /**
34     * @param string $type 'extensions' or 'skins'
35     *
36     * TODO we need some way to limit the number of extensions returning?
37     *
38     * @return array|bool array of extensions in order of popularity or false on failure
39     */
40    public function getPopularList( $type ) {
41        global $wgExtDistGraphiteRenderApi, $wgServerName, $wgStatsdMetricPrefix;
42        if ( !$wgExtDistGraphiteRenderApi ) {
43            return false;
44        }
45
46        $objectcachefactory = MediaWikiServices::getInstance()->getObjectCacheFactory();
47
48        $cache = $objectcachefactory->getInstance( CACHE_ANYTHING );
49        $cacheKey = $this->getCacheKey( $cache, $type );
50
51        $cachedValue = $cache->get( $cacheKey );
52        if ( $cachedValue ) {
53            $this->logger->debug( "Retrieved PopularList of $type from cache" );
54            // @phan-suppress-next-line PhanCoalescingNeverNull $cachedValue can be null
55            return $cachedValue ?? false;
56        }
57
58        $metric = "$wgStatsdMetricPrefix.extdist.$type.*.*.sum";
59        $requestParams = [
60            'target' => 'sortByMaxima(groupByNode(summarize(' . $metric . ',"4w","sum",true),3,"sum"))',
61            'format' => 'json',
62            'from' => '-4w',
63            'until' => 'now',
64        ];
65
66        $httpOptions = [
67            'userAgent' => "$wgServerName - ExtensionDistributor  - MediaWiki Extension",
68        ];
69        $url = $wgExtDistGraphiteRenderApi . '/?' . http_build_query( $requestParams );
70        $req = MediaWikiServices::getInstance()->getHttpRequestFactory()
71            ->create( $url, $httpOptions, __METHOD__ );
72        $status = $req->execute();
73        if ( !$status->isOK() ) {
74            $this->logger->error( "Could not fetch popularList of $type from graphite, " .
75                "received: {$status}"
76            );
77            // Store a negative cache entry so we don't hammer graphite
78            $cache->set( $cacheKey, null, 60 * 60 );
79            return false;
80        }
81
82        $info = wfObjectToArray( FormatJson::decode( $req->getContent(), true ), true );
83        '@phan-var array[] $info';
84
85        $popularList = [];
86        foreach ( $info as $dataSet ) {
87            $popularList[] = $dataSet['target'];
88        }
89        if ( !$popularList ) {
90            $this->logger->error( "Graphite result resulted in empty PopularList of $type" );
91            // Store a negative cache entry so we don't hammer graphite
92            $cache->set( $cacheKey, null, 60 * 60 );
93            return false;
94        }
95
96        $popularList = array_slice( $popularList, 0, 15 );
97
98        // Cache list for 1 day
99        $cacheSuccess = $cache->set( $cacheKey, $popularList, 60 * 60 * 24 );
100        if ( !$cacheSuccess ) {
101            $this->logger->error( "Could not store PopularList of $type in cache." );
102        }
103
104        return $popularList;
105    }
106
107    /**
108     * @param BagOStuff $cache
109     * @param string $type
110     * @return string
111     */
112    private function getCacheKey( BagOStuff $cache, $type ) {
113        return $cache->makeKey( 'extdist', 'GraphiteStats', $type, 'PopularList' );
114    }
115
116    public function clearCache() {
117        $cache = MediaWikiServices::getInstance()
118            ->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
119        $typesToClear = [
120            ExtDistProvider::EXTENSIONS,
121            ExtDistProvider::SKINS
122        ];
123        foreach ( $typesToClear as $type ) {
124            $success = $cache->delete( $this->getCacheKey( $cache, $type ) );
125            if ( !$success ) {
126                $this->logger->error( "Failed to clear PopularList cache for $type" );
127            }
128        }
129    }
130
131}