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