Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.27% covered (warning)
86.27%
44 / 51
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClearUserWatchlistJob
86.27% covered (warning)
86.27%
44 / 51
50.00% covered (danger)
50.00%
2 / 4
10.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newForUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
86.36% covered (warning)
86.36%
38 / 44
0.00% covered (danger)
0.00%
0 / 1
7.12
 getDeduplicationInfo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3use MediaWiki\MainConfigNames;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\User\UserIdentity;
6
7/**
8 * Job to clear a users watchlist in batches.
9 *
10 * @since 1.31
11 * @ingroup JobQueue
12 * @author Addshore
13 */
14class ClearUserWatchlistJob extends Job implements GenericParameterJob {
15    /**
16     * @param array $params
17     *  - userId,         The ID for the user whose watchlist is being cleared.
18     *  - maxWatchlistId, The maximum wl_id at the time the job was first created,
19     */
20    public function __construct( array $params ) {
21        parent::__construct( 'clearUserWatchlist', $params );
22
23        $this->removeDuplicates = true;
24    }
25
26    /**
27     * @param UserIdentity $user User to clear the watchlist for.
28     * @param int $maxWatchlistId The maximum wl_id at the time the job was first created.
29     *
30     * @return ClearUserWatchlistJob
31     */
32    public static function newForUser( UserIdentity $user, $maxWatchlistId ) {
33        return new self( [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] );
34    }
35
36    public function run() {
37        $updateRowsPerQuery = MediaWikiServices::getInstance()->getMainConfig()->get(
38            MainConfigNames::UpdateRowsPerQuery );
39        $userId = $this->params['userId'];
40        $maxWatchlistId = $this->params['maxWatchlistId'];
41        $batchSize = $updateRowsPerQuery;
42
43        $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
44        $dbw = $loadBalancer->getConnectionRef( DB_PRIMARY );
45        $dbr = $loadBalancer->getConnectionRef( DB_REPLICA );
46
47        // Wait before lock to try to reduce time waiting in the lock.
48        if ( !$loadBalancer->waitForPrimaryPos( $dbr ) ) {
49            $this->setLastError( 'Timed out waiting for replica to catch up before lock' );
50            return false;
51        }
52
53        // Use a named lock so that jobs for this user see each others' changes
54        $lockKey = "{{$dbw->getDomainID()}}:ClearUserWatchlist:$userId"; // per-wiki
55        $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
56        if ( !$scopedLock ) {
57            $this->setLastError( "Could not acquire lock '$lockKey'" );
58            return false;
59        }
60
61        if ( !$loadBalancer->waitForPrimaryPos( $dbr ) ) {
62            $this->setLastError( 'Timed out waiting for replica to catch up within lock' );
63            return false;
64        }
65
66        // Clear any stale REPEATABLE-READ snapshot
67        $dbr->flushSnapshot( __METHOD__ );
68
69        $watchlistIds = $dbr->newSelectQueryBuilder()
70            ->select( 'wl_id' )
71            ->from( 'watchlist' )
72            ->where( [ 'wl_user' => $userId ] )
73            ->andWhere( $dbr->expr( 'wl_id', '<=', $maxWatchlistId ) )
74            ->limit( $batchSize )
75            ->caller( __METHOD__ )->fetchFieldValues();
76        if ( count( $watchlistIds ) == 0 ) {
77            return true;
78        }
79
80        $dbw->newDeleteQueryBuilder()
81            ->deleteFrom( 'watchlist' )
82            ->where( [ 'wl_id' => $watchlistIds ] )
83            ->caller( __METHOD__ )->execute();
84        if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::WatchlistExpiry ) ) {
85            $dbw->newDeleteQueryBuilder()
86                ->deleteFrom( 'watchlist_expiry' )
87                ->where( [ 'we_item' => $watchlistIds ] )
88                ->caller( __METHOD__ )->execute();
89        }
90
91        // Commit changes and remove lock before inserting next job.
92        $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
93        $lbf->commitPrimaryChanges( __METHOD__ );
94        unset( $scopedLock );
95
96        if ( count( $watchlistIds ) === (int)$batchSize ) {
97            // Until we get less results than the limit, recursively push
98            // the same job again.
99            MediaWikiServices::getInstance()->getJobQueueGroup()->push( new self( $this->getParams() ) );
100        }
101
102        return true;
103    }
104
105    public function getDeduplicationInfo() {
106        $info = parent::getDeduplicationInfo();
107        // This job never has a namespace or title so we can't use it for deduplication
108        unset( $info['namespace'] );
109        unset( $info['title'] );
110        return $info;
111    }
112
113}