Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.89% |
53 / 90 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
SiteStatsUpdate | |
59.55% |
53 / 89 |
|
40.00% |
2 / 5 |
39.44 | |
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 | |
78.43% |
40 / 51 |
|
0.00% |
0 / 1 |
11.00 | |||
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 | |
30 | /** |
31 | * Class for handling updates to the site_stats table |
32 | */ |
33 | class 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 */ |
214 | class_alias( SiteStatsUpdate::class, 'SiteStatsUpdate' ); |