Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.14% covered (warning)
57.14%
52 / 91
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteStatsUpdate
57.78% covered (warning)
57.78%
52 / 90
40.00% covered (danger)
40.00%
2 / 5
42.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 merge
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 factory
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 doUpdate
76.47% covered (warning)
76.47%
39 / 51
0.00% covered (danger)
0.00%
0 / 1
11.30
 cacheUpdate
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Deferred;
8
9use MediaWiki\MainConfigNames;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\SiteStats\SiteStats;
12use UnexpectedValueException;
13use Wikimedia\Assert\Assert;
14use Wikimedia\Rdbms\IDatabase;
15use Wikimedia\Rdbms\RawSQLValue;
16
17/**
18 * Class for handling updates to the site_stats table
19 */
20class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
21    /** @var int */
22    protected $edits = 0;
23    /** @var int */
24    protected $pages = 0;
25    /** @var int */
26    protected $articles = 0;
27    /** @var int */
28    protected $users = 0;
29    /** @var int */
30    protected $images = 0;
31
32    private const SHARDS_OFF = 1;
33    public const SHARDS_ON = 10;
34
35    /** @var string[] Map of (table column => counter type) */
36    private const COUNTERS = [
37        'ss_total_edits'   => 'edits',
38        'ss_total_pages'   => 'pages',
39        'ss_good_articles' => 'articles',
40        'ss_users'         => 'users',
41        'ss_images'        => 'images'
42    ];
43
44    /**
45     * @deprecated since 1.39 Use SiteStatsUpdate::factory() instead.
46     */
47    public function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
48        $this->edits = $edits;
49        $this->articles = $good;
50        $this->pages = $pages;
51        $this->users = $users;
52    }
53
54    public function merge( MergeableUpdate $update ) {
55        /** @var SiteStatsUpdate $update */
56        Assert::parameterType( __CLASS__, $update, '$update' );
57        '@phan-var SiteStatsUpdate $update';
58
59        foreach ( self::COUNTERS as $field ) {
60            $this->$field += $update->$field;
61        }
62    }
63
64    /**
65     * @param int[] $deltas Map of (counter type => integer delta) e.g.
66     *         ```
67     *         SiteStatsUpdate::factory( [
68     *            'edits'    => 10,
69     *            'articles' => 2,
70     *            'pages'    => 7,
71     *            'users'    => 5,
72     *        ] );
73     *         ```
74     * @return SiteStatsUpdate
75     * @throws UnexpectedValueException
76     */
77    public static function factory( array $deltas ) {
78        $update = new self( 0, 0, 0 );
79
80        foreach ( $deltas as $name => $unused ) {
81            if ( !in_array( $name, self::COUNTERS ) ) { // T187585
82                throw new UnexpectedValueException( __METHOD__ . ": no field called '$name'" );
83            }
84        }
85
86        foreach ( self::COUNTERS as $field ) {
87            $update->$field = $deltas[$field] ?? 0;
88        }
89
90        return $update;
91    }
92
93    public function doUpdate() {
94        $services = MediaWikiServices::getInstance();
95        $stats = $services->getStatsFactory();
96        $shards = $services->getMainConfig()->get( MainConfigNames::MultiShardSiteStats )
97            ? self::SHARDS_ON
98            : self::SHARDS_OFF;
99
100        $deltaByType = [];
101        foreach ( self::COUNTERS as $type ) {
102            $delta = $this->$type;
103            $deltaByType[$type] = $delta;
104
105            // T392258: This is an operational metric for site activity and server load,
106            // e.g. edit submissions and account creations.
107            // When MediaWiki adjusts the "total" downward, e.g. after a re-count or
108            // page deletion, we should ignore that. We have to anyway, as Prometheus
109            // requires counters to monotonically increase.
110            // https://prometheus.io/docs/concepts/metric_types/#counter
111            if ( $delta > 0 ) {
112                $stats->getCounter( 'site_stats_total' )
113                    ->setLabel( 'engagement', $type )
114                    ->incrementBy( $delta );
115            }
116        }
117
118        ( new AutoCommitUpdate(
119            $services->getConnectionProvider()->getPrimaryDatabase(),
120            __METHOD__,
121            static function ( IDatabase $dbw, $fname ) use ( $deltaByType, $shards ) {
122                $set = [];
123                $initValues = [];
124                if ( $shards > 1 ) {
125                    $shard = mt_rand( 1, $shards );
126                } else {
127                    $shard = 1;
128                }
129
130                $hasNegativeDelta = false;
131                foreach ( self::COUNTERS as $field => $type ) {
132                    $delta = (int)$deltaByType[$type];
133                    $initValues[$field] = $delta;
134                    if ( $delta > 0 ) {
135                        $set[$field] = new RawSQLValue( $dbw->addIdentifierQuotes( $field ) . '+' . abs( $delta ) );
136                    } elseif ( $delta < 0 ) {
137                        $hasNegativeDelta = true;
138                        $set[$field] = new RawSQLValue( $dbw->buildGreatest(
139                            [ 'new' => $dbw->addIdentifierQuotes( $field ) ],
140                            abs( $delta )
141                        ) . '-' . abs( $delta ) );
142                    }
143                }
144
145                if ( $set ) {
146                    if ( $hasNegativeDelta ) {
147                        $dbw->newUpdateQueryBuilder()
148                            ->update( 'site_stats' )
149                            ->set( $set )
150                            ->where( [ 'ss_row_id' => $shard ] )
151                            ->caller( $fname )->execute();
152                    } else {
153                        $dbw->newInsertQueryBuilder()
154                            ->insertInto( 'site_stats' )
155                            ->row( $initValues + [ 'ss_row_id' => $shard ] )
156                            ->onDuplicateKeyUpdate()
157                            ->uniqueIndexFields( [ 'ss_row_id' ] )
158                            ->set( $set )
159                            ->caller( $fname )->execute();
160                    }
161                }
162            }
163        ) )->doUpdate();
164
165        // Invalidate cache used by parser functions
166        SiteStats::unload();
167    }
168
169    /**
170     * @param IDatabase $dbw
171     * @return bool|mixed
172     */
173    public static function cacheUpdate( IDatabase $dbw ) {
174        $services = MediaWikiServices::getInstance();
175        $config = $services->getMainConfig();
176
177        $rcl = $services->getRecentChangeLookup();
178        $dbr = $services->getConnectionProvider()->getReplicaDatabase( false, 'vslow' );
179        # Get non-bot users that did some recent action other than making accounts.
180        # If account creation is included, the number gets inflated ~20+ fold on enwiki.
181        $activeUsers = $dbr->newSelectQueryBuilder()
182            ->select( 'COUNT(DISTINCT rc_actor)' )
183            ->from( 'recentchanges' )
184            ->join( 'actor', 'actor', 'actor_id=rc_actor' )
185            ->where( [
186                $dbr->expr( 'rc_source', '=', $rcl->getPrimarySources() ),
187                $dbr->expr( 'actor_user', '!=', null ),
188                $dbr->expr( 'rc_bot', '=', 0 ),
189                $dbr->expr( 'rc_log_type', '!=', 'newusers' )->or( 'rc_log_type', '=', null ),
190                $dbr->expr( 'rc_timestamp', '>=',
191                    $dbr->timestamp( time() - $config->get( MainConfigNames::ActiveUserDays ) * 24 * 3600 ) )
192            ] )
193            ->caller( __METHOD__ )
194            ->fetchField();
195        $dbw->newUpdateQueryBuilder()
196            ->update( 'site_stats' )
197            ->set( [ 'ss_active_users' => intval( $activeUsers ) ] )
198            ->where( [ 'ss_row_id' => 1 ] )
199            ->caller( __METHOD__ )->execute();
200
201        // Invalid cache used by parser functions
202        SiteStats::unload();
203
204        return $activeUsers;
205    }
206}
207
208/** @deprecated class alias since 1.42 */
209class_alias( SiteStatsUpdate::class, 'SiteStatsUpdate' );