use Wikimedia\Rdbms\IDatabase;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MediaWiki\Linker\LinkTarget;
-use MediaWiki\MediaWikiServices;
use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\LBFactory;
use Wikimedia\ScopedCallback;
+use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\LoadBalancer;
/**
*/
class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
+ /**
+ * @var ILBFactory
+ */
+ private $lbFactory;
+
/**
* @var LoadBalancer
*/
private $stats;
/**
- * @param LoadBalancer $loadBalancer
+ * @param ILBFactory $lbFactory
* @param HashBagOStuff $cache
* @param ReadOnlyMode $readOnlyMode
* @param int $updateRowsPerQuery
*/
public function __construct(
- LoadBalancer $loadBalancer,
+ ILBFactory $lbFactory,
HashBagOStuff $cache,
ReadOnlyMode $readOnlyMode,
$updateRowsPerQuery
) {
- $this->loadBalancer = $loadBalancer;
+ $this->lbFactory = $lbFactory;
+ $this->loadBalancer = $lbFactory->getMainLB();
$this->cache = $cache;
$this->readOnlyMode = $readOnlyMode;
$this->stats = new NullStatsdDataFactory();
return $visitingWatchers;
}
+ /**
+ * @param User $user
+ * @param TitleValue[] $titles
+ * @return bool
+ * @throws MWException
+ */
+ public function removeWatchBatchForUser( User $user, array $titles ) {
+ if ( $this->readOnlyMode->isReadOnly() ) {
+ return false;
+ }
+ if ( $user->isAnon() ) {
+ return false;
+ }
+ if ( !$titles ) {
+ return true;
+ }
+
+ $rows = $this->getTitleDbKeysGroupedByNamespace( $titles );
+ $this->uncacheTitlesForUser( $user, $titles );
+
+ $dbw = $this->getConnectionRef( DB_MASTER );
+ $ticket = count( $titles ) > $this->updateRowsPerQuery ?
+ $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
+ $affectedRows = 0;
+
+ // Batch delete items per namespace.
+ foreach ( $rows as $namespace => $namespaceTitles ) {
+ $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
+ foreach ( $rowBatches as $toDelete ) {
+ $dbw->delete( 'watchlist', [
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $namespace,
+ 'wl_title' => $toDelete
+ ], __METHOD__ );
+ $affectedRows += $dbw->affectedRows();
+ if ( $ticket ) {
+ $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ }
+ }
+
+ return (bool)$affectedRows;
+ }
+
/**
* @since 1.27
* @param LinkTarget[] $targets
* @since 1.27
* @param User $user
* @param LinkTarget $target
+ * @throws MWException
*/
public function addWatch( User $user, LinkTarget $target ) {
$this->addWatchBatchForUser( $user, [ $target ] );
* @param User $user
* @param LinkTarget[] $targets
* @return bool
+ * @throws MWException
*/
public function addWatchBatchForUser( User $user, array $targets ) {
if ( $this->readOnlyMode->isReadOnly() ) {
return false;
}
- // Only loggedin user can have a watchlist
+ // Only logged-in user can have a watchlist
if ( $user->isAnon() ) {
return false;
}
}
$dbw = $this->getConnectionRef( DB_MASTER );
- foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
+ $ticket = count( $targets ) > $this->updateRowsPerQuery ?
+ $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
+ $affectedRows = 0;
+ $rowBatches = array_chunk( $rows, $this->updateRowsPerQuery );
+ foreach ( $rowBatches as $toInsert ) {
// Use INSERT IGNORE to avoid overwriting the notification timestamp
// if there's already an entry for this page
$dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
+ $affectedRows += $dbw->affectedRows();
+ if ( $ticket ) {
+ $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
}
// Update process cache to ensure skin doesn't claim that the current
// page is unwatched in the response of action=watch itself (T28292).
$this->cache( $item );
}
- return true;
+ return (bool)$affectedRows;
}
/**
* @param User $user
* @param LinkTarget $target
* @return bool
+ * @throws MWException
*/
public function removeWatch( User $user, LinkTarget $target ) {
- // Only logged in user can have a watchlist
- if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
- return false;
- }
-
- $this->uncache( $user, $target );
-
- $dbw = $this->getConnectionRef( DB_MASTER );
- $dbw->delete( 'watchlist',
- [
- 'wl_user' => $user->getId(),
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- ], __METHOD__
- );
- $success = (bool)$dbw->affectedRows();
-
- return $success;
+ return $this->removeWatchBatchForUser( $user, [ $target ] );
}
/**
$fname = __METHOD__;
DeferredUpdates::addCallableUpdate(
function () use ( $timestamp, $watchers, $target, $fname ) {
- global $wgUpdateRowsPerQuery;
-
$dbw = $this->getConnectionRef( DB_MASTER );
- $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+ $ticket = $this->lbFactory->getEmptyTransactionTicket( $fname );
- $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
+ $watchersChunks = array_chunk( $watchers, $this->updateRowsPerQuery );
foreach ( $watchersChunks as $watchersChunk ) {
$dbw->update( 'watchlist',
[ /* SET */
], $fname
);
if ( count( $watchersChunks ) > 1 ) {
- $factory->commitAndWaitForReplication(
- __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ]
+ $this->lbFactory->commitAndWaitForReplication(
+ $fname, $ticket, [ 'domain' => $dbw->getDomainID() ]
);
}
}
}
}
+ /**
+ * @param TitleValue[] $titles
+ * @return array
+ */
+ private function getTitleDbKeysGroupedByNamespace( array $titles ) {
+ $rows = [];
+ foreach ( $titles as $title ) {
+ // Group titles by namespace.
+ $rows[ $title->getNamespace() ][] = $title->getDBkey();
+ }
+ return $rows;
+ }
+
+ /**
+ * @param User $user
+ * @param Title[] $titles
+ */
+ private function uncacheTitlesForUser( User $user, array $titles ) {
+ foreach ( $titles as $title ) {
+ $this->uncache( $user, $title );
+ }
+ }
+
}