From: addshore Date: Mon, 14 Mar 2016 21:07:39 +0000 (+0000) Subject: Introduce ClearUserWatchlistJob X-Git-Tag: 1.31.0-rc.0~1382^2 X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=commitdiff_plain;h=989ba875620690e37aeea3fdd862e9eff0ef1691;p=lhc%2Fweb%2Fwiklou.git Introduce ClearUserWatchlistJob Change-Id: Icea573a10078ea3f09dc2e4e9fdc737bf639935d --- diff --git a/autoload.php b/autoload.php index 50055349df..5ee4447332 100644 --- a/autoload.php +++ b/autoload.php @@ -265,6 +265,7 @@ $wgAutoloadLocalClasses = [ 'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php', 'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php', 'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php', + 'ClearUserWatchlistJob' => __DIR__ . '/includes/jobqueue/jobs/ClearUserWatchlistJob.php', 'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php', 'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php', 'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 3cd7ef181a..e3785869ae 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -7428,6 +7428,7 @@ $wgJobClasses = [ 'refreshLinksDynamic' => 'RefreshLinksJob', 'activityUpdateJob' => 'ActivityUpdateJob', 'categoryMembershipChange' => 'CategoryMembershipChangeJob', + 'clearUserWatchlist' => 'ClearUserWatchlistJob', 'cdnPurge' => 'CdnPurgeJob', 'enqueue' => 'EnqueueJob', // local queue for multi-DC setups 'null' => 'NullJob' diff --git a/includes/jobqueue/jobs/ClearUserWatchlistJob.php b/includes/jobqueue/jobs/ClearUserWatchlistJob.php new file mode 100644 index 0000000000..17c4b665e2 --- /dev/null +++ b/includes/jobqueue/jobs/ClearUserWatchlistJob.php @@ -0,0 +1,119 @@ + $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] + ); + } + + /** + * @param Title|null $title Not used by this job. + * @param array $params + * - batchSize, Number of watchlist entries to remove at once. + * - userId, The ID for the user whose watchlist is being cleared. + * - maxWatchlistId, The maximum wl_id at the time the job was first created, + */ + public function __construct( Title $title = null, array $params ) { + if ( !array_key_exists( 'batchSize', $params ) ) { + $params['batchSize'] = 1000; + } + + parent::__construct( + 'clearUserWatchlist', + SpecialPage::getTitleFor( 'EditWatchlist', 'clear' ), + $params + ); + + $this->removeDuplicates = true; + } + + public function run() { + $userId = $this->params['userId']; + $maxWatchlistId = $this->params['maxWatchlistId']; + + $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $dbw = $loadBalancer->getConnection( DB_MASTER ); + $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] ); + + // Wait before lock to try to reduce time waiting in the lock. + if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) { + $this->setLastError( 'Timed out while waiting for slave to catch up before lock' ); + return false; + } + + // Use a named lock so that jobs for this user see each others' changes + $lockKey = "ClearUserWatchlistJob:$userId"; + $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 ); + if ( !$scopedLock ) { + $this->setLastError( "Could not acquire lock '$lockKey'" ); + return false; + } + + if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) { + $this->setLastError( 'Timed out while waiting for slave to catch up within lock' ); + return false; + } + + // Clear any stale REPEATABLE-READ snapshot + $dbr->flushSnapshot( __METHOD__ ); + + $watchlistIds = $dbr->selectFieldValues( + 'watchlist', + 'wl_id', + [ + 'wl_user' => $userId, + 'wl_id <= ' . $maxWatchlistId + ], + __METHOD__, + [ + 'ORDER BY' => 'wl_id ASC', + 'LIMIT' => $this->params['batchSize'], + ] + ); + + if ( count( $watchlistIds ) == 0 ) { + return true; + } + + $dbw->delete( 'watchlist', [ 'wl_id' => $watchlistIds ], __METHOD__ ); + + // Commit changes and remove lock before inserting next job. + $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbf->commitMasterChanges( __METHOD__ ); + unset( $scopedLock ); + + if ( count( $watchlistIds ) == $this->params['batchSize'] ) { + JobQueueGroup::singleton()->push( new self( $this->getTitle(), $this->getParams() ) ); + } + + return true; + } + + public function getDeduplicationInfo() { + $info = parent::getDeduplicationInfo(); + // This job never has a namespace or title so we can't use it for deduplication + unset( $info['namespace'] ); + unset( $info['title'] ); + return $info; + } + +} diff --git a/includes/watcheditem/WatchedItemStore.php b/includes/watcheditem/WatchedItemStore.php index 094297cd8d..f29bd479c1 100644 --- a/includes/watcheditem/WatchedItemStore.php +++ b/includes/watcheditem/WatchedItemStore.php @@ -212,6 +212,33 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] ); } + /** + * Queues a job that will clear the users watchlist using the Job Queue. + * + * @since 1.31 + * + * @param User $user + */ + public function clearUserWatchedItemsUsingJobQueue( User $user ) { + $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() ); + // TODO inject me. + JobQueueGroup::singleton()->push( $job ); + } + + /** + * @since 1.31 + * @return int The maximum current wl_id + */ + public function getMaxId() { + $dbr = $this->getConnectionRef( DB_REPLICA ); + return (int)$dbr->selectField( + 'watchlist', + 'MAX(wl_id)', + '', + __METHOD__ + ); + } + /** * @since 1.31 */ diff --git a/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php b/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php new file mode 100644 index 0000000000..385ecb75e8 --- /dev/null +++ b/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php @@ -0,0 +1,78 @@ +runJobs(); + JobQueueGroup::destroySingletons(); + } + + private function getUser() { + return self::$users['ClearUserWatchlistJobTestUser']->getUser(); + } + + private function runJobs( $jobLimit = 9999 ) { + $runJobs = new RunJobs; + $runJobs->loadParamsAndArgs( null, [ 'quiet' => true, 'maxjobs' => $jobLimit ] ); + $runJobs->execute(); + } + + private function getWatchedItemStore() { + return MediaWikiServices::getInstance()->getWatchedItemStore(); + } + + public function testRun() { + $user = $this->getUser(); + $watchedItemStore = $this->getWatchedItemStore(); + + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'A' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'A' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'B' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'B' ) ); + + $maxId = $watchedItemStore->getMaxId(); + + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'C' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'C' ) ); + + JobQueueGroup::singleton()->push( + new ClearUserWatchlistJob( + null, + [ + 'userId' => $user->getId(), + 'batchSize' => 2, + 'maxWatchlistId' => $maxId, + ] + ) + ); + + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 6, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 4, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 0, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) ); + + $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 0, 'C' ) ) ); + $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 1, 'C' ) ) ); + } + +}