$store = new WatchedItemStore(
$services->getDBLoadBalancer(),
new HashBagOStuff( [ 'maxKeys' => 100 ] ),
- $services->getReadOnlyMode()
+ $services->getReadOnlyMode(),
+ $services->getMainConfig()->get( 'UpdateRowsPerQuery' )
);
$store->setStatsdDataFactory( $services->getStatsdDataFactory() );
* Database interaction & caching
* TODO caching should be factored out into a CachingWatchedItemStore class
*
- * Uses database because this uses User::isAnon
- *
- * @group Database
- *
* @author Addshore
* @since 1.27
*/
*/
private $revisionGetTimestampFromIdCallback;
+ /**
+ * @var int
+ */
+ private $updateRowsPerQuery;
+
/**
* @var StatsdDataFactoryInterface
*/
* @param LoadBalancer $loadBalancer
* @param HashBagOStuff $cache
* @param ReadOnlyMode $readOnlyMode
+ * @param int $updateRowsPerQuery
*/
public function __construct(
LoadBalancer $loadBalancer,
HashBagOStuff $cache,
- ReadOnlyMode $readOnlyMode
+ ReadOnlyMode $readOnlyMode,
+ $updateRowsPerQuery
) {
$this->loadBalancer = $loadBalancer;
$this->cache = $cache;
$this->readOnlyMode = $readOnlyMode;
$this->stats = new NullStatsdDataFactory();
- $this->deferredUpdatesAddCallableUpdateCallback = [ DeferredUpdates::class, 'addCallableUpdate' ];
- $this->revisionGetTimestampFromIdCallback = [ Revision::class, 'getTimestampFromId' ];
+ $this->deferredUpdatesAddCallableUpdateCallback =
+ [ DeferredUpdates::class, 'addCallableUpdate' ];
+ $this->revisionGetTimestampFromIdCallback =
+ [ Revision::class, 'getTimestampFromId' ];
+ $this->updateRowsPerQuery = $updateRowsPerQuery;
}
/**
return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
}
+ /**
+ * Deletes ALL watched items for the given user when under
+ * $updateRowsPerQuery entries exist.
+ *
+ * @since 1.30
+ *
+ * @param User $user
+ *
+ * @return bool true on success, false when too many items are watched
+ */
+ public function clearUserWatchedItems( User $user ) {
+ if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) {
+ return false;
+ }
+
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
+ $dbw->delete(
+ 'watchlist',
+ [ 'wl_user' => $user->getId() ],
+ __METHOD__
+ );
+ $this->uncacheAllItemsForUser( $user );
+
+ return true;
+ }
+
+ private function uncacheAllItemsForUser( User $user ) {
+ $userId = $user->getId();
+ foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
+ foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
+ if ( array_key_exists( $userId, $userIndex ) ) {
+ $this->cache->delete( $userIndex[$userId] );
+ unset( $this->cacheIndex[$ns][$dbKey][$userId] );
+ }
+ }
+ }
+
+ // Cleanup empty cache keys
+ foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
+ foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
+ if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
+ unset( $this->cacheIndex[$ns][$dbKey] );
+ }
+ }
+ if ( empty( $this->cacheIndex[$ns] ) ) {
+ unset( $this->cacheIndex[$ns] );
+ }
+ }
+ }
+
/**
* Queues a job that will clear the users watchlist using the Job Queue.
*
);
}
+ public function testWatchBatchAndClearItems() {
+ $user = $this->getUser();
+ $title1 = Title::newFromText( 'WatchedItemStoreIntegrationTestPage1' );
+ $title2 = Title::newFromText( 'WatchedItemStoreIntegrationTestPage2' );
+ $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+
+ $store->addWatchBatchForUser( $user, [ $title1, $title2 ] );
+
+ $this->assertTrue( $store->isWatched( $user, $title1 ) );
+ $this->assertTrue( $store->isWatched( $user, $title2 ) );
+
+ $store->clearUserWatchedItems( $user );
+
+ $this->assertFalse( $store->isWatched( $user, $title1 ) );
+ $this->assertFalse( $store->isWatched( $user, $title2 ) );
+ }
+
public function testUpdateResetAndSetNotificationTimestamp() {
$user = $this->getUser();
$otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
use MediaWiki\Linker\LinkTarget;
use Wikimedia\Rdbms\LoadBalancer;
use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
/**
* @author Addshore
return new WatchedItemStore(
$loadBalancer,
$cache,
- $readOnlyMode
+ $readOnlyMode,
+ 1000
);
}
+ public function testClearWatchedItems() {
+ $user = $this->getMockNonAnonUserWithId( 7 );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectField' )
+ ->with(
+ 'watchlist',
+ 'COUNT(*)',
+ [
+ 'wl_user' => $user->getId(),
+ ],
+ $this->isType( 'string' )
+ )
+ ->will( $this->returnValue( 12 ) );
+ $mockDb->expects( $this->once() )
+ ->method( 'delete' )
+ ->with(
+ 'watchlist',
+ [ 'wl_user' => 7 ],
+ $this->isType( 'string' )
+ );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->never() )->method( 'set' );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( 'RM-KEY' );
+
+ $store = $this->newWatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache,
+ $this->getMockReadOnlyMode()
+ );
+ TestingAccessWrapper::newFromObject( $store )
+ ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ];
+
+ $this->assertTrue( $store->clearUserWatchedItems( $user ) );
+ }
+
+ public function testClearWatchedItems_tooManyItemsWatched() {
+ $user = $this->getMockNonAnonUserWithId( 7 );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectField' )
+ ->with(
+ 'watchlist',
+ 'COUNT(*)',
+ [
+ 'wl_user' => $user->getId(),
+ ],
+ $this->isType( 'string' )
+ )
+ ->will( $this->returnValue( 99999 ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )->method( 'get' );
+ $mockCache->expects( $this->never() )->method( 'set' );
+ $mockCache->expects( $this->never() )->method( 'delete' );
+
+ $store = $this->newWatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache,
+ $this->getMockReadOnlyMode()
+ );
+
+ $this->assertFalse( $store->clearUserWatchedItems( $user ) );
+ }
+
public function testCountWatchedItems() {
$user = $this->getMockNonAnonUserWithId( 1 );