Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.78% |
55 / 92 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
SiteStatsUpdate | |
60.44% |
55 / 91 |
|
40.00% |
2 / 5 |
38.06 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
merge | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
factory | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
doUpdate | |
79.25% |
42 / 53 |
|
0.00% |
0 / 1 |
10.89 | |||
cacheUpdate | |
0.00% |
0 / 24 |
|
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 | |
21 | namespace MediaWiki\Deferred; |
22 | |
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; |
30 | |
31 | /** |
32 | * Class for handling updates to the site_stats table |
33 | */ |
34 | class 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 */ |
217 | class_alias( SiteStatsUpdate::class, 'SiteStatsUpdate' ); |