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.
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.
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
21 namespace MediaWiki\Deferred
;
23 use MediaWiki\MainConfigNames
;
24 use MediaWiki\MediaWikiServices
;
25 use MediaWiki\SiteStats\SiteStats
;
26 use UnexpectedValueException
;
27 use Wikimedia\Assert\Assert
;
28 use Wikimedia\Rdbms\IDatabase
;
29 use Wikimedia\Rdbms\RawSQLValue
;
32 * Class for handling updates to the site_stats table
34 class SiteStatsUpdate
implements DeferrableUpdate
, MergeableUpdate
{
40 protected $articles = 0;
44 protected $images = 0;
46 private const SHARDS_OFF
= 1;
47 public const SHARDS_ON
= 10;
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'
59 * @deprecated since 1.39 Use SiteStatsUpdate::factory() instead.
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;
68 public function merge( MergeableUpdate
$update ) {
69 /** @var SiteStatsUpdate $update */
70 Assert
::parameterType( __CLASS__
, $update, '$update' );
71 '@phan-var SiteStatsUpdate $update';
73 foreach ( self
::COUNTERS
as $field ) {
74 $this->$field +
= $update->$field;
79 * @param int[] $deltas Map of (counter type => integer delta) e.g.
81 * SiteStatsUpdate::factory( [
88 * @return SiteStatsUpdate
89 * @throws UnexpectedValueException
91 public static function factory( array $deltas ) {
92 $update = new self( 0, 0, 0 );
94 foreach ( $deltas as $name => $unused ) {
95 if ( !in_array( $name, self
::COUNTERS
) ) { // T187585
96 throw new UnexpectedValueException( __METHOD__
. ": no field called '$name'" );
100 foreach ( self
::COUNTERS
as $field ) {
101 $update->$field = $deltas[$field] ??
0;
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
;
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 );
121 $deltaByType[$type] = $delta;
124 ( new AutoCommitUpdate(
125 $services->getConnectionProvider()->getPrimaryDatabase(),
127 static function ( IDatabase
$dbw, $fname ) use ( $deltaByType, $shards ) {
131 $shard = mt_rand( 1, $shards );
136 $hasNegativeDelta = false;
137 foreach ( self
::COUNTERS
as $field => $type ) {
138 $delta = (int)$deltaByType[$type];
139 $initValues[$field] = $delta;
141 $set[$field] = new RawSQLValue( $dbw->buildGreatest(
142 [ $field => $dbw->addIdentifierQuotes( $field ) . '+' . abs( $delta ) ],
145 } elseif ( $delta < 0 ) {
146 $hasNegativeDelta = true;
147 $set[$field] = new RawSQLValue( $dbw->buildGreatest(
148 [ 'new' => $dbw->addIdentifierQuotes( $field ) . '-' . abs( $delta ) ],
155 if ( $hasNegativeDelta ) {
156 $dbw->newUpdateQueryBuilder()
157 ->update( 'site_stats' )
159 ->where( [ 'ss_row_id' => $shard ] )
160 ->caller( $fname )->execute();
162 $dbw->newInsertQueryBuilder()
163 ->insertInto( 'site_stats' )
164 ->row( array_merge( [ 'ss_row_id' => $shard ], $initValues ) )
165 ->onDuplicateKeyUpdate()
166 ->uniqueIndexFields( [ 'ss_row_id' ] )
168 ->caller( $fname )->execute();
174 // Invalidate cache used by parser functions
179 * @param IDatabase $dbw
182 public static function cacheUpdate( IDatabase
$dbw ) {
183 $services = MediaWikiServices
::getInstance();
184 $config = $services->getMainConfig();
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' )
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 ) )
201 ->caller( __METHOD__
)
203 $dbw->newUpdateQueryBuilder()
204 ->update( 'site_stats' )
205 ->set( [ 'ss_active_users' => intval( $activeUsers ) ] )
206 ->where( [ 'ss_row_id' => 1 ] )
207 ->caller( __METHOD__
)->execute();
209 // Invalid cache used by parser functions
216 /** @deprecated class alias since 1.42 */
217 class_alias( SiteStatsUpdate
::class, 'SiteStatsUpdate' );