Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TotalsLookup
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 makeKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotals
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 updateStats
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 touchCategoryCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 touchAllCategoriesCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Linter;
22
23use IBufferingStatsdDataFactory;
24use MediaWiki\Config\ServiceOptions;
25use MediaWiki\WikiMap\WikiMap;
26use WANObjectCache;
27use Wikimedia\Rdbms\Database as MWDatabase;
28
29/**
30 * Lookup to find and cache the total amount of
31 * lint errors in each category
32 */
33class TotalsLookup {
34    public const CONSTRUCTOR_OPTIONS = [
35        'LinterStatsdSampleFactor',
36    ];
37
38    private ServiceOptions $options;
39    private WANObjectCache $cache;
40    private IBufferingStatsdDataFactory $statsdDataFactory;
41    private CategoryManager $categoryManager;
42    private Database $database;
43
44    /**
45     * @param ServiceOptions $options
46     * @param WANObjectCache $cache
47     * @param IBufferingStatsdDataFactory $statsdDataFactory
48     * @param CategoryManager $categoryManager
49     * @param Database $database
50     */
51    public function __construct(
52        ServiceOptions $options,
53        WANObjectCache $cache,
54        IBufferingStatsdDataFactory $statsdDataFactory,
55        CategoryManager $categoryManager,
56        Database $database
57    ) {
58        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
59        $this->options = $options;
60        $this->cache = $cache;
61        $this->statsdDataFactory = $statsdDataFactory;
62        $this->categoryManager = $categoryManager;
63        $this->database = $database;
64    }
65
66    /**
67     * @param string $cat
68     * @return string
69     */
70    private function makeKey( $cat ) {
71        return $this->cache->makeKey( 'linter', 'total', $cat );
72    }
73
74    /**
75     * Get the totals for every category in the database
76     *
77     * @return array
78     */
79    public function getTotals(): array {
80        $cats = $this->categoryManager->getVisibleCategories();
81        $fetchedTotals = false;
82        $totals = [];
83        foreach ( $cats as $cat ) {
84            $totals[$cat] = $this->cache->getWithSetCallback(
85                $this->makeKey( $cat ),
86                WANObjectCache::TTL_INDEFINITE,
87                function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $cat, &$fetchedTotals ) {
88                    $setOpts += MWDatabase::getCacheSetOptions(
89                        $this->database->getDBConnectionRef( DB_REPLICA )
90                    );
91                    if ( $fetchedTotals === false ) {
92                        $fetchedTotals = $this->database->getTotals();
93                    }
94                    return $fetchedTotals[$cat];
95                },
96                [
97                    'checkKeys' => [
98                        $this->cache->makeKey( 'linter', 'totals' ),
99                        $this->makeKey( $cat ),
100                    ],
101                    'lockTSE' => 30,
102                ]
103            );
104        }
105        return $totals;
106    }
107
108    /**
109     * Send stats to statsd and update totals cache
110     *
111     * @param array $changes
112     */
113    public function updateStats( array $changes ) {
114        $linterStatsdSampleFactor = $this->options->get( 'LinterStatsdSampleFactor' );
115
116        if ( $linterStatsdSampleFactor === false ) {
117            // Don't send to statsd, but update cache with $changes
118            $raw = $changes['added'];
119            foreach ( $changes['deleted'] as $cat => $count ) {
120                if ( isset( $raw[$cat] ) ) {
121                    $raw[$cat] -= $count;
122                } else {
123                    // Negative value
124                    $raw[$cat] = 0 - $count;
125                }
126            }
127
128            foreach ( $raw as $cat => $count ) {
129                if ( $count != 0 ) {
130                    // There was a change in counts, invalidate the cache
131                    $this->touchCategoryCache( $cat );
132                }
133            }
134            return;
135        } elseif ( mt_rand( 1, $linterStatsdSampleFactor ) != 1 ) {
136            return;
137        }
138
139        $totals = $this->database->getTotals();
140        $wiki = WikiMap::getCurrentWikiId();
141        $stats = $this->statsdDataFactory;
142        foreach ( $totals as $name => $count ) {
143            $stats->gauge( "linter.category.$name.$wiki", $count );
144        }
145        $stats->gauge( "linter.totals.$wiki", array_sum( $totals ) );
146
147        $this->touchAllCategoriesCache();
148    }
149
150    /**
151     * Have a single category be recalculated
152     *
153     * @param string $cat category name
154     */
155    public function touchCategoryCache( $cat ) {
156        $this->cache->touchCheckKey( $this->makeKey( $cat ) );
157    }
158
159    /**
160     * Have all categories be recalculated
161     */
162    public function touchAllCategoriesCache() {
163        $this->cache->touchCheckKey( $this->cache->makeKey( 'linter', 'totals' ) );
164    }
165}