Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 143 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
RecentChangesUpdateJob | |
0.00% |
0 / 143 |
|
0.00% |
0 / 6 |
506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
newPurgeJob | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
newCacheUpdateJob | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
purgeExpiredRows | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
30 | |||
updateActiveUsers | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
110 |
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 | use MediaWiki\Deferred\SiteStatsUpdate; |
22 | use MediaWiki\HookContainer\HookRunner; |
23 | use MediaWiki\MainConfigNames; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\SpecialPage\SpecialPage; |
26 | use MediaWiki\Title\Title; |
27 | |
28 | /** |
29 | * Puurge expired rows from the recentchanges table. |
30 | * |
31 | * @ingroup JobQueue |
32 | * @since 1.25 |
33 | */ |
34 | class RecentChangesUpdateJob extends Job { |
35 | public function __construct( Title $title, array $params ) { |
36 | parent::__construct( 'recentChangesUpdate', $title, $params ); |
37 | |
38 | if ( !isset( $params['type'] ) ) { |
39 | throw new InvalidArgumentException( "Missing 'type' parameter." ); |
40 | } |
41 | |
42 | $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND; |
43 | $this->removeDuplicates = true; |
44 | } |
45 | |
46 | /** |
47 | * @return RecentChangesUpdateJob |
48 | */ |
49 | final public static function newPurgeJob() { |
50 | return new self( |
51 | SpecialPage::getTitleFor( 'Recentchanges' ), [ 'type' => 'purge' ] |
52 | ); |
53 | } |
54 | |
55 | /** |
56 | * @return RecentChangesUpdateJob |
57 | * @since 1.26 |
58 | */ |
59 | final public static function newCacheUpdateJob() { |
60 | return new self( |
61 | SpecialPage::getTitleFor( 'Recentchanges' ), [ 'type' => 'cacheUpdate' ] |
62 | ); |
63 | } |
64 | |
65 | public function run() { |
66 | if ( $this->params['type'] === 'purge' ) { |
67 | $this->purgeExpiredRows(); |
68 | } elseif ( $this->params['type'] === 'cacheUpdate' ) { |
69 | $this->updateActiveUsers(); |
70 | } else { |
71 | throw new InvalidArgumentException( |
72 | "Invalid 'type' parameter '{$this->params['type']}'." ); |
73 | } |
74 | |
75 | return true; |
76 | } |
77 | |
78 | protected function purgeExpiredRows() { |
79 | $services = MediaWikiServices::getInstance(); |
80 | $rcMaxAge = $services->getMainConfig()->get( |
81 | MainConfigNames::RCMaxAge ); |
82 | $updateRowsPerQuery = $services->getMainConfig()->get( |
83 | MainConfigNames::UpdateRowsPerQuery ); |
84 | $dbProvider = $services->getConnectionProvider(); |
85 | $dbw = $dbProvider->getPrimaryDatabase(); |
86 | $lockKey = $dbw->getDomainID() . ':recentchanges-prune'; |
87 | if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) { |
88 | // already in progress |
89 | return; |
90 | } |
91 | $ticket = $dbProvider->getEmptyTransactionTicket( __METHOD__ ); |
92 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
93 | $cutoff = $dbw->timestamp( time() - $rcMaxAge ); |
94 | $rcQuery = RecentChange::getQueryInfo(); |
95 | do { |
96 | $rcIds = []; |
97 | $rows = []; |
98 | $res = $dbw->select( |
99 | $rcQuery['tables'], |
100 | $rcQuery['fields'], |
101 | [ $dbw->expr( 'rc_timestamp', '<', $cutoff ) ], |
102 | __METHOD__, |
103 | [ 'LIMIT' => $updateRowsPerQuery ], |
104 | $rcQuery['joins'] |
105 | ); |
106 | foreach ( $res as $row ) { |
107 | $rcIds[] = $row->rc_id; |
108 | $rows[] = $row; |
109 | } |
110 | if ( $rcIds ) { |
111 | $dbw->newDeleteQueryBuilder() |
112 | ->deleteFrom( 'recentchanges' ) |
113 | ->where( [ 'rc_id' => $rcIds ] ) |
114 | ->caller( __METHOD__ )->execute(); |
115 | $hookRunner->onRecentChangesPurgeRows( $rows ); |
116 | // There might be more, so try waiting for replica DBs |
117 | if ( !$dbProvider->commitAndWaitForReplication( |
118 | __METHOD__, $ticket, [ 'timeout' => 3 ] |
119 | ) ) { |
120 | // Another job will continue anyway |
121 | break; |
122 | } |
123 | } |
124 | } while ( $rcIds ); |
125 | |
126 | $dbw->unlock( $lockKey, __METHOD__ ); |
127 | } |
128 | |
129 | protected function updateActiveUsers() { |
130 | $activeUserDays = MediaWikiServices::getInstance()->getMainConfig()->get( |
131 | MainConfigNames::ActiveUserDays ); |
132 | |
133 | // Users that made edits at least this many days ago are "active" |
134 | $days = $activeUserDays; |
135 | // Pull in the full window of active users in this update |
136 | $window = $activeUserDays * 86400; |
137 | |
138 | $dbProvider = MediaWikiServices::getInstance()->getConnectionProvider(); |
139 | $dbw = $dbProvider->getPrimaryDatabase(); |
140 | $ticket = $dbProvider->getEmptyTransactionTicket( __METHOD__ ); |
141 | |
142 | $lockKey = $dbw->getDomainID() . '-activeusers'; |
143 | if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) { |
144 | // Exclusive update (avoids duplicate entries)… it's usually fine to just |
145 | // drop out here, if the Job is already running. |
146 | return; |
147 | } |
148 | |
149 | // Long-running queries expected |
150 | $dbw->setSessionOptions( [ 'connTimeout' => 900 ] ); |
151 | |
152 | $nowUnix = time(); |
153 | // Get the last-updated timestamp for the cache |
154 | $cTime = $dbw->newSelectQueryBuilder() |
155 | ->select( 'qci_timestamp' ) |
156 | ->from( 'querycache_info' ) |
157 | ->where( [ 'qci_type' => 'activeusers' ] ) |
158 | ->caller( __METHOD__ )->fetchField(); |
159 | $cTimeUnix = $cTime ? (int)wfTimestamp( TS_UNIX, $cTime ) : 1; |
160 | |
161 | // Pick the date range to fetch from. This is normally from the last |
162 | // update to till the present time, but has a limited window. |
163 | // If the window is limited, multiple runs are need to fully populate it. |
164 | $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 ); |
165 | $eTimestamp = min( $sTimestamp + $window, $nowUnix ); |
166 | |
167 | // Get all the users active since the last update |
168 | $res = $dbw->newSelectQueryBuilder() |
169 | ->select( [ 'actor_name', 'lastedittime' => 'MAX(rc_timestamp)' ] ) |
170 | ->from( 'recentchanges' ) |
171 | ->join( 'actor', null, 'actor_id=rc_actor' ) |
172 | ->where( [ |
173 | $dbw->expr( 'actor_user', '!=', null ), // actual accounts |
174 | $dbw->expr( 'rc_type', '!=', RC_EXTERNAL ), // no wikidata |
175 | $dbw->expr( 'rc_log_type', '=', null )->or( 'rc_log_type', '!=', 'newusers' ), |
176 | $dbw->expr( 'rc_timestamp', '>=', $dbw->timestamp( $sTimestamp ) ), |
177 | $dbw->expr( 'rc_timestamp', '<=', $dbw->timestamp( $eTimestamp ) ), |
178 | ] ) |
179 | ->groupBy( 'actor_name' ) |
180 | ->orderBy( 'NULL' ) // avoid filesort |
181 | ->caller( __METHOD__ )->fetchResultSet(); |
182 | |
183 | $names = []; |
184 | foreach ( $res as $row ) { |
185 | $names[$row->actor_name] = $row->lastedittime; |
186 | } |
187 | |
188 | // Find which of the recently active users are already accounted for |
189 | if ( count( $names ) ) { |
190 | $res = $dbw->newSelectQueryBuilder() |
191 | ->select( [ 'user_name' => 'qcc_title' ] ) |
192 | ->from( 'querycachetwo' ) |
193 | ->where( [ |
194 | 'qcc_type' => 'activeusers', |
195 | 'qcc_namespace' => NS_USER, |
196 | 'qcc_title' => array_map( 'strval', array_keys( $names ) ), |
197 | $dbw->expr( 'qcc_value', '>=', $nowUnix - $days * 86400 ), |
198 | ] ) |
199 | ->caller( __METHOD__ )->fetchResultSet(); |
200 | // Note: In order for this to be actually consistent, we would need |
201 | // to update these rows with the new lastedittime. |
202 | foreach ( $res as $row ) { |
203 | unset( $names[$row->user_name] ); |
204 | } |
205 | } |
206 | |
207 | // Insert the users that need to be added to the list |
208 | if ( count( $names ) ) { |
209 | $newRows = []; |
210 | foreach ( $names as $name => $lastEditTime ) { |
211 | $newRows[] = [ |
212 | 'qcc_type' => 'activeusers', |
213 | 'qcc_namespace' => NS_USER, |
214 | 'qcc_title' => $name, |
215 | 'qcc_value' => (int)wfTimestamp( TS_UNIX, $lastEditTime ), |
216 | 'qcc_namespacetwo' => 0, // unused |
217 | 'qcc_titletwo' => '' // unused |
218 | ]; |
219 | } |
220 | foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) { |
221 | $dbw->newInsertQueryBuilder() |
222 | ->insertInto( 'querycachetwo' ) |
223 | ->rows( $rowBatch ) |
224 | ->caller( __METHOD__ )->execute(); |
225 | $dbProvider->commitAndWaitForReplication( __METHOD__, $ticket ); |
226 | } |
227 | } |
228 | |
229 | // If a transaction was already started, it might have an old |
230 | // snapshot, so kludge the timestamp range back as needed. |
231 | $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() ); |
232 | |
233 | // Touch the data freshness timestamp |
234 | $dbw->newReplaceQueryBuilder() |
235 | ->replaceInto( 'querycache_info' ) |
236 | ->row( [ |
237 | 'qci_type' => 'activeusers', |
238 | 'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ), // not always $now |
239 | ] ) |
240 | ->uniqueIndexFields( [ 'qci_type' ] ) |
241 | ->caller( __METHOD__ )->execute(); |
242 | |
243 | // Rotate out users that have not edited in too long (according to old data set) |
244 | $dbw->newDeleteQueryBuilder() |
245 | ->deleteFrom( 'querycachetwo' ) |
246 | ->where( [ |
247 | 'qcc_type' => 'activeusers', |
248 | $dbw->expr( 'qcc_value', '<', $nowUnix - $days * 86400 ) // TS_UNIX |
249 | ] ) |
250 | ->caller( __METHOD__ )->execute(); |
251 | |
252 | if ( !MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MiserMode ) ) { |
253 | SiteStatsUpdate::cacheUpdate( $dbw ); |
254 | } |
255 | |
256 | $dbw->unlock( $lockKey, __METHOD__ ); |
257 | } |
258 | } |