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