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