Follow-up 6d4e1547: Hard-deprecate these functions
[lhc/web/wiklou.git] / includes / SiteStats.php
1 <?php
2 /**
3 * Accessors and mutators for the site-wide statistics.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 use Wikimedia\Rdbms\Database;
24 use Wikimedia\Rdbms\IDatabase;
25 use MediaWiki\MediaWikiServices;
26 use Wikimedia\Rdbms\LoadBalancer;
27
28 /**
29 * Static accessor class for site_stats and related things
30 */
31 class SiteStats {
32 /** @var stdClass */
33 private static $row;
34
35 /**
36 * Trigger a reload next time a field is accessed
37 */
38 public static function unload() {
39 self::$row = null;
40 }
41
42 protected static function load() {
43 if ( self::$row === null ) {
44 self::$row = self::loadAndLazyInit();
45 }
46 }
47
48 /**
49 * @return stdClass
50 */
51 protected static function loadAndLazyInit() {
52 $config = MediaWikiServices::getInstance()->getMainConfig();
53
54 $lb = self::getLB();
55 $dbr = $lb->getConnection( DB_REPLICA );
56 wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
57 $row = self::doLoadFromDB( $dbr );
58
59 if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
60 // Might have just been initialized during this request? Underflow?
61 wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
62 $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
63 }
64
65 if ( !self::isRowSane( $row ) ) {
66 if ( $config->get( 'MiserMode' ) ) {
67 // Start off with all zeroes, assuming that this is a new wiki or any
68 // repopulations where done manually via script.
69 SiteStatsInit::doPlaceholderInit();
70 } else {
71 // Normally the site_stats table is initialized at install time.
72 // Some manual construction scenarios may leave the table empty or
73 // broken, however, for instance when importing from a dump into a
74 // clean schema with mwdumper.
75 wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
76 SiteStatsInit::doAllAndCommit( $dbr );
77 }
78
79 $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
80 }
81
82 if ( !self::isRowSane( $row ) ) {
83 wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
84 // Always return a row-like object
85 $row = self::salvageInsaneRow( $row );
86 }
87
88 return $row;
89 }
90
91 /**
92 * @return int
93 */
94 public static function edits() {
95 self::load();
96
97 return (int)self::$row->ss_total_edits;
98 }
99
100 /**
101 * @return int
102 */
103 public static function articles() {
104 self::load();
105
106 return (int)self::$row->ss_good_articles;
107 }
108
109 /**
110 * @return int
111 */
112 public static function pages() {
113 self::load();
114
115 return (int)self::$row->ss_total_pages;
116 }
117
118 /**
119 * @return int
120 */
121 public static function users() {
122 self::load();
123
124 return (int)self::$row->ss_users;
125 }
126
127 /**
128 * @return int
129 */
130 public static function activeUsers() {
131 self::load();
132
133 return (int)self::$row->ss_active_users;
134 }
135
136 /**
137 * @return int
138 */
139 public static function images() {
140 self::load();
141
142 return (int)self::$row->ss_images;
143 }
144
145 /**
146 * Find the number of users in a given user group.
147 * @param string $group Name of group
148 * @return int
149 */
150 public static function numberingroup( $group ) {
151 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
152
153 return $cache->getWithSetCallback(
154 $cache->makeKey( 'SiteStats', 'groupcounts', $group ),
155 $cache::TTL_HOUR,
156 function ( $oldValue, &$ttl, array &$setOpts ) use ( $group ) {
157 $dbr = self::getLB()->getConnection( DB_REPLICA );
158 $setOpts += Database::getCacheSetOptions( $dbr );
159
160 return (int)$dbr->selectField(
161 'user_groups',
162 'COUNT(*)',
163 [
164 'ug_group' => $group,
165 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
166 ],
167 __METHOD__
168 );
169 },
170 [ 'pcTTL' => $cache::TTL_PROC_LONG ]
171 );
172 }
173
174 /**
175 * Total number of jobs in the job queue.
176 * @return int
177 */
178 public static function jobs() {
179 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
180
181 return $cache->getWithSetCallback(
182 $cache->makeKey( 'SiteStats', 'jobscount' ),
183 $cache::TTL_MINUTE,
184 function ( $oldValue, &$ttl, array &$setOpts ) {
185 try{
186 $jobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
187 } catch ( JobQueueError $e ) {
188 $jobs = 0;
189 }
190 return $jobs;
191 },
192 [ 'pcTTL' => $cache::TTL_PROC_LONG ]
193 );
194 }
195
196 /**
197 * @param int $ns
198 * @return int
199 */
200 public static function pagesInNs( $ns ) {
201 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
202
203 return $cache->getWithSetCallback(
204 $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
205 $cache::TTL_HOUR,
206 function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns ) {
207 $dbr = self::getLB()->getConnection( DB_REPLICA );
208 $setOpts += Database::getCacheSetOptions( $dbr );
209
210 return (int)$dbr->selectField(
211 'page',
212 'COUNT(*)',
213 [ 'page_namespace' => $ns ],
214 __METHOD__
215 );
216 },
217 [ 'pcTTL' => $cache::TTL_PROC_LONG ]
218 );
219 }
220
221 /**
222 * @return array
223 */
224 public static function selectFields() {
225 return [
226 'ss_total_edits',
227 'ss_good_articles',
228 'ss_total_pages',
229 'ss_users',
230 'ss_active_users',
231 'ss_images',
232 ];
233 }
234
235 /**
236 * @param IDatabase $db
237 * @return stdClass|bool
238 */
239 private static function doLoadFromDB( IDatabase $db ) {
240 return $db->selectRow(
241 'site_stats',
242 self::selectFields(),
243 [ 'ss_row_id' => 1 ],
244 __METHOD__
245 );
246 }
247
248 /**
249 * Is the provided row of site stats sane, or should it be regenerated?
250 *
251 * Checks only fields which are filled by SiteStatsInit::refresh.
252 *
253 * @param bool|object $row
254 * @return bool
255 */
256 private static function isRowSane( $row ) {
257 if ( $row === false
258 || $row->ss_total_pages < $row->ss_good_articles
259 || $row->ss_total_edits < $row->ss_total_pages
260 ) {
261 return false;
262 }
263 // Now check for underflow/overflow
264 foreach ( [
265 'ss_total_edits',
266 'ss_good_articles',
267 'ss_total_pages',
268 'ss_users',
269 'ss_images',
270 ] as $member ) {
271 if ( $row->$member < 0 ) {
272 return false;
273 }
274 }
275
276 return true;
277 }
278
279 /**
280 * @param stdClass|bool $row
281 * @return stdClass
282 */
283 private static function salvageInsaneRow( $row ) {
284 $map = $row ? (array)$row : [];
285 // Fill in any missing values with zero
286 $map += array_fill_keys( self::selectFields(), 0 );
287 // Convert negative values to zero
288 foreach ( $map as $field => $value ) {
289 $map[$field] = max( 0, $value );
290 }
291
292 return (object)$row;
293 }
294
295 /**
296 * @return LoadBalancer
297 */
298 private static function getLB() {
299 return MediaWikiServices::getInstance()->getDBLoadBalancer();
300 }
301 }