3 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
12 use Wikimedia\Assert\Assert;
18 use Wikimedia\ScopedCallback;
36 'WatchlistExpiryMaxDuration',
158 $this->updateRowsPerQuery = $options->
get(
'UpdateRowsPerQuery' );
159 $this->expiryEnabled = $options->
get(
'WatchlistExpiry' );
160 $this->maxExpiryDuration = $options->
get(
'WatchlistExpiryMaxDuration' );
169 $this->deferredUpdatesAddCallableUpdateCallback =
170 [ DeferredUpdates::class,
'addCallableUpdate' ];
173 $this->hookRunner =
new HookRunner( $hookContainer );
177 $this->latestUpdateCache =
new HashBagOStuff( [
'maxKeys' => 3 ] );
199 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
201 'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
205 $this->deferredUpdatesAddCallableUpdateCallback = $callback;
206 return new ScopedCallback(
function () use ( $previousValue ) {
207 $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
212 return $this->
cache->makeKey(
215 (
string)$user->
getId()
223 $this->
cache->set( $key, $item );
224 $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
225 $this->stats->increment(
'WatchedItemStore.cache' );
231 $this->stats->increment(
'WatchedItemStore.uncache' );
235 $this->stats->increment(
'WatchedItemStore.uncacheLinkTarget' );
240 $this->stats->increment(
'WatchedItemStore.uncacheLinkTarget.items' );
241 $this->
cache->delete( $key );
246 $this->stats->increment(
'WatchedItemStore.uncacheUser' );
247 foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
248 foreach ( $dbKeyArray as $dbKey => $userArray ) {
249 if ( isset( $userArray[$user->
getId()] ) ) {
250 $this->stats->increment(
'WatchedItemStore.uncacheUser.items' );
251 $this->
cache->delete( $userArray[$user->
getId()] );
257 $this->latestUpdateCache->delete( $pageSeenKey );
258 $this->stash->delete( $pageSeenKey );
277 return $this->loadBalancer->getConnectionRef( $dbIndex, [
'watchlist' ] );
295 $dbw = $this->loadBalancer->getConnectionRef(
DB_MASTER );
297 if ( $this->expiryEnabled ) {
298 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
300 $wlIds = $dbw->selectFieldValues(
'watchlist',
'wl_id', [
301 'wl_user' => $user->
getId()
308 [
'wl_id' => $wlIds ],
314 [
'we_item' => $wlIds ],
318 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
322 [
'wl_user' => $user->
getId() ],
337 $userId = $user->
getId();
338 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
339 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
340 if ( array_key_exists( $userId, $userIndex ) ) {
341 $this->
cache->delete( $userIndex[$userId] );
342 unset( $this->cacheIndex[$ns][$dbKey][$userId] );
348 foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
349 foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
350 if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
351 unset( $this->cacheIndex[$ns][$dbKey] );
354 if ( empty( $this->cacheIndex[$ns] ) ) {
355 unset( $this->cacheIndex[$ns] );
369 $this->queueGroup->push(
$job );
376 $max = mt_getrandmax();
377 if ( mt_rand( 0, $max ) < $max * $watchlistPurgeRate ) {
389 return (
int)
$dbr->selectField(
404 $tables = [
'watchlist' ];
405 $conds = [
'wl_user' => $user->
getId() ];
408 if ( $this->expiryEnabled ) {
409 $tables[] =
'watchlist_expiry';
410 $joinConds[
'watchlist_expiry' ] = [
'LEFT JOIN',
'wl_id = we_item' ];
411 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
414 $return = (int)
$dbr->selectField(
433 $tables = [
'watchlist' ];
440 if ( $this->expiryEnabled ) {
441 $tables[] =
'watchlist_expiry';
442 $joinConds[
'watchlist_expiry' ] = [
'LEFT JOIN',
'wl_id = we_item' ];
443 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
446 $return = (int)
$dbr->selectField(
466 $tables = [
'watchlist' ];
470 'wl_notificationtimestamp >= ' .
471 $dbr->addQuotes(
$dbr->timestamp( $threshold ) ) .
472 ' OR wl_notificationtimestamp IS NULL'
476 if ( $this->expiryEnabled ) {
477 $tables[] =
'watchlist_expiry';
478 $joinConds[
'watchlist_expiry' ] = [
'LEFT JOIN',
'wl_id = we_item' ];
479 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
482 $visitingWatchers = (int)
$dbr->selectField(
491 return $visitingWatchers;
500 if ( $this->readOnlyMode->isReadOnly() ) {
514 $ticket = count( $titles ) > $this->updateRowsPerQuery ?
515 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) :
null;
519 foreach ( $rows as $namespace => $namespaceTitles ) {
520 $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
521 foreach ( $rowBatches as $toDelete ) {
523 $wlIds = $dbw->selectFieldValues(
'watchlist',
'wl_id', [
524 'wl_user' => $user->
getId(),
525 'wl_namespace' => $namespace,
526 'wl_title' => $toDelete
533 [
'wl_id' => $wlIds ],
536 $affectedRows += $dbw->affectedRows();
538 if ( $this->expiryEnabled ) {
541 [
'we_item' => $wlIds ],
544 $affectedRows += $dbw->affectedRows();
549 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
554 return (
bool)$affectedRows;
564 $dbOptions = [
'GROUP BY' => [
'wl_namespace',
'wl_title' ] ];
568 if ( array_key_exists(
'minimumWatchers', $options ) ) {
569 $dbOptions[
'HAVING'] =
'COUNT(*) >= ' . (int)$options[
'minimumWatchers'];
572 $lb = $this->linkBatchFactory->newLinkBatch( $targets );
574 $tables = [
'watchlist' ];
575 $conds = [ $lb->constructSet(
'wl',
$dbr ) ];
578 if ( $this->expiryEnabled ) {
579 $tables[] =
'watchlist_expiry';
580 $joinConds[
'watchlist_expiry' ] = [
'LEFT JOIN',
'wl_id = we_item' ];
581 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
586 [
'wl_title',
'wl_namespace',
'watchers' =>
'COUNT(*)' ],
594 foreach ( $targets as $linkTarget ) {
595 $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
598 foreach (
$res as $row ) {
599 $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
612 array $targetsWithVisitThresholds,
613 $minimumWatchers =
null
615 if ( $targetsWithVisitThresholds === [] ) {
624 $dbOptions = [
'GROUP BY' => [
'wl_namespace',
'wl_title' ] ];
625 if ( $minimumWatchers !==
null ) {
626 $dbOptions[
'HAVING'] =
'COUNT(*) >= ' . (int)$minimumWatchers;
629 $tables = [
'watchlist' ];
632 if ( $this->expiryEnabled ) {
633 $tables[] =
'watchlist_expiry';
634 $joinConds[
'watchlist_expiry' ] = [
'LEFT JOIN',
'wl_id = we_item' ];
635 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
640 [
'wl_namespace',
'wl_title',
'watchers' =>
'COUNT(*)' ],
648 foreach ( $targetsWithVisitThresholds as list( $target ) ) {
650 $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
653 foreach (
$res as $row ) {
654 $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
657 return $watcherCounts;
669 array $targetsWithVisitThresholds
671 $missingTargets = [];
672 $namespaceConds = [];
673 foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
674 if ( $threshold ===
null ) {
675 $missingTargets[] = $target;
679 $namespaceConds[$target->getNamespace()][] = $db->
makeList( [
680 'wl_title = ' . $db->
addQuotes( $target->getDBkey() ),
683 'wl_notificationtimestamp IS NULL'
689 foreach ( $namespaceConds as $namespace => $pageConds ) {
691 'wl_namespace = ' . $namespace,
696 if ( $missingTargets ) {
697 $lb = $this->linkBatchFactory->newLinkBatch( $missingTargets );
698 $conds[] = $lb->constructSet(
'wl', $db );
715 $cached = $this->
getCached( $user, $target );
716 if ( $cached && !$cached->isExpired() ) {
717 $this->stats->increment(
'WatchedItemStore.getWatchedItem.cached' );
720 $this->stats->increment(
'WatchedItemStore.getWatchedItem.load' );
732 return $item ? $item[0] :
false;
752 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
762 foreach ( $rows as $row ) {
763 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
765 $this->
cache( $item );
779 $options += [
'forWrite' => false ];
780 $vars = [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ];
783 if ( array_key_exists(
'sort', $options ) ) {
785 ( in_array( $options[
'sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
786 '$options[\'sort\']',
787 'must be SORT_ASC or SORT_DESC'
789 $dbOptions[
'ORDER BY'][] =
"wl_namespace {$options['sort']}";
790 if ( $this->expiryEnabled
791 && array_key_exists(
'sortByExpiry', $options )
792 && $options[
'sortByExpiry']
795 $vars[
'wl_has_expiry'] = $db->conditional(
'we_expiry IS NULL', 0, 1 );
798 $dbOptions[
'ORDER BY'][] =
"wl_has_expiry DESC";
799 $dbOptions[
'ORDER BY'][] =
"we_expiry ASC";
802 $dbOptions[
'ORDER BY'][] =
"wl_title {$options['sort']}";
813 foreach (
$res as $row ) {
814 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
819 return $watchedItems;
838 $row->wl_notificationtimestamp, $user, $target ),
862 $dbMethod =
'select';
863 $conds = [
'wl_user' => $user->
getId() ];
867 $dbMethod =
'selectRow';
868 $conds = array_merge( $conds, [
869 'wl_namespace' => $target->getNamespace(),
870 'wl_title' => $target->getDBkey(),
874 foreach ( $target as $linkTarget ) {
877 'wl_namespace' => $linkTarget->getNamespace(),
878 'wl_title' => $linkTarget->getDBkey(),
887 if ( $this->expiryEnabled ) {
888 $vars[] =
'we_expiry';
891 return $db->{$dbMethod}(
892 [
'watchlist',
'watchlist_expiry' ],
897 [
'watchlist_expiry' => [
'LEFT JOIN', [
'wl_id = we_item' ] ] ]
901 return $db->{$dbMethod}(
917 return (
bool)$this->getWatchedItem( $user, $target );
928 $item = $this->getWatchedItem( $user, $target );
929 return $item && $item->getExpiry();
940 foreach ( $targets as $target ) {
941 $timestamps[$target->getNamespace()][$target->getDBkey()] =
false;
949 foreach ( $targets as $target ) {
950 $cachedItem = $this->getCached( $user, $target );
952 $timestamps[$target->getNamespace()][$target->getDBkey()] =
953 $cachedItem->getNotificationTimestamp();
955 $targetsToLoad[] = $target;
959 if ( !$targetsToLoad ) {
965 $lb = $this->linkBatchFactory->newLinkBatch( $targetsToLoad );
968 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
970 $lb->constructSet(
'wl',
$dbr ),
971 'wl_user' => $user->
getId(),
976 foreach (
$res as $row ) {
977 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
978 $timestamps[$row->wl_namespace][$row->wl_title] =
979 $this->getLatestNotificationTimestamp(
980 $row->wl_notificationtimestamp, $user, $target );
995 $this->addWatchBatchForUser( $user, [ $target ], $expiry );
997 if ( $this->expiryEnabled && !$expiry ) {
1002 $this->loadWatchedItem( $user, $target );
1006 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS_ISO_8601 );
1013 $this->cache( $item );
1033 ?
string $expiry =
null
1035 if ( $this->readOnlyMode->isReadOnly() ) {
1046 $expiry = ExpiryDef::normalizeUsingMaxExpiry( $expiry, $this->maxExpiryDuration, TS_ISO_8601 );
1048 foreach ( $targets as $target ) {
1050 'wl_user' => $user->
getId(),
1051 'wl_namespace' => $target->getNamespace(),
1052 'wl_title' => $target->getDBkey(),
1053 'wl_notificationtimestamp' =>
null,
1055 $this->uncache( $user, $target );
1058 $dbw = $this->getConnectionRef(
DB_MASTER );
1059 $ticket = count( $targets ) > $this->updateRowsPerQuery ?
1060 $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) :
null;
1062 $rowBatches = array_chunk( $rows, $this->updateRowsPerQuery );
1063 foreach ( $rowBatches as $toInsert ) {
1066 $dbw->insert(
'watchlist', $toInsert, __METHOD__, [
'IGNORE' ] );
1067 $affectedRows += $dbw->affectedRows();
1069 if ( $this->expiryEnabled ) {
1070 $affectedRows += $this->updateOrDeleteExpiries( $dbw, $user->
getId(), $toInsert, $expiry );
1074 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1078 return (
bool)$affectedRows;
1094 ?
string $expiry =
null
1103 foreach ( $rows as $row ) {
1106 'wl_user' => $userId,
1107 'wl_namespace' => $row[
'wl_namespace'],
1108 'wl_title' => $row[
'wl_title']
1129 return $this->updateExpiries( $dbw, $expiry, $cond );
1143 $wlIds = (array)$dbw->
selectFieldValues(
'watchlist',
'wl_id', $cond, __METHOD__ );
1147 $weRows = array_map(
function ( $wlId ) use ( $expiry, $dbw ) {
1150 'we_expiry' => $expiry
1159 [
'we_expiry' => $expiry ],
1173 return $this->removeWatchBatchForUser( $user, [ $target ] );
1197 if ( !$user->
isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1203 $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
1207 $rows = $this->getTitleDbKeysGroupedByNamespace( $targets );
1209 $dbw = $this->getConnectionRef(
DB_MASTER );
1210 if ( $timestamp !==
null ) {
1211 $timestamp = $dbw->timestamp( $timestamp );
1213 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1214 $affectedSinceWait = 0;
1217 foreach ( $rows as $namespace => $namespaceTitles ) {
1218 $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
1219 foreach ( $rowBatches as $toUpdate ) {
1222 [
'wl_notificationtimestamp' => $timestamp ],
1224 'wl_user' => $user->
getId(),
1225 'wl_namespace' => $namespace,
1226 'wl_title' => $toUpdate
1230 $affectedSinceWait += $dbw->affectedRows();
1232 if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
1233 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1234 $affectedSinceWait = 0;
1239 $this->uncacheUser( $user );
1248 if ( $timestamp ===
null ) {
1252 $seenTimestamps = $this->getPageSeenTimestamps( $user );
1255 $seenTimestamps->get( $this->getPageSeenKey( $target ) ) >= $timestamp
1278 'userId' => $user->
getId(),
'timestamp' => $timestamp,
'casTime' => time()
1284 $this->deferredUpdatesAddCallableUpdateCallback,
1285 function () use (
$job ) {
1301 $dbw = $this->getConnectionRef(
DB_MASTER );
1302 $selectTables = [
'watchlist' ];
1304 'wl_user != ' . intval( $editor->
getId() ),
1307 'wl_notificationtimestamp IS NULL',
1311 if ( $this->expiryEnabled ) {
1312 $selectTables[] =
'watchlist_expiry';
1313 $selectConds[] =
'we_expiry IS NULL OR we_expiry > ' .
1314 $dbw->addQuotes( $dbw->timestamp() );
1315 $selectJoin = [
'watchlist_expiry' => [
'LEFT JOIN',
'wl_id = we_item' ] ];
1318 $uids = $dbw->selectFieldValues(
1327 $watchers = array_map(
'intval', $uids );
1330 $fname = __METHOD__;
1332 function () use ( $timestamp, $watchers, $target, $fname ) {
1333 $dbw = $this->getConnectionRef(
DB_MASTER );
1334 $ticket = $this->lbFactory->getEmptyTransactionTicket( $fname );
1336 $watchersChunks = array_chunk( $watchers, $this->updateRowsPerQuery );
1337 foreach ( $watchersChunks as $watchersChunk ) {
1338 $dbw->update(
'watchlist',
1340 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
1342 'wl_user' => $watchersChunk,
1347 if ( count( $watchersChunks ) > 1 ) {
1348 $this->lbFactory->commitAndWaitForReplication(
1349 $fname, $ticket, [
'domain' => $dbw->getDomainID() ]
1353 $this->uncacheLinkTarget( $target );
1355 DeferredUpdates::POSTSEND,
1377 if ( $this->readOnlyMode->isReadOnly() || !$user->
isRegistered() ) {
1382 $userObj = $this->userFactory->newFromId( $user->
getId() );
1384 if ( !$this->hookRunner->onBeforeResetNotificationTimestamp(
1385 $userObj, $titleObj, $force, $oldid )
1389 if ( !$userObj->equals( $user ) ) {
1392 if ( !$titleObj->equals(
$title ) ) {
1397 if ( $force !=
'force' ) {
1398 $item = $this->loadWatchedItem( $user,
$title );
1399 if ( !$item || $item->getNotificationTimestamp() ===
null ) {
1408 $latestRev = $this->revisionLookup->getRevisionByTitle(
$title );
1410 $id = $latestRev->getId();
1412 $seenTime = $latestRev->getTimestamp();
1415 if ( $seenTime ===
null ) {
1416 $seenTime = $this->revisionLookup->getTimestampFromId( $id );
1420 $this->stash->merge(
1421 $this->getPageSeenTimestampsKey( $user ),
1422 function (
$cache, $key, $current ) use (
$title, $seenTime ) {
1424 $subKey = $this->getPageSeenKey(
$title );
1426 if ( $seenTime > $value->get( $subKey ) ) {
1428 $value->set( $subKey, $seenTime );
1429 $this->latestUpdateCache->set( $key, $value, BagOStuff::TTL_PROC_LONG );
1430 } elseif ( $seenTime ===
false ) {
1433 $this->latestUpdateCache->set( $key, $value, BagOStuff::TTL_PROC_LONG );
1447 'type' =>
'updateWatchlistNotification',
1448 'userid' => $user->
getId(),
1449 'notifTime' => $this->getNotificationTimestamp( $user,
$title, $item, $force, $oldid ),
1454 $this->queueGroup->lazyPush(
$job );
1456 $this->uncache( $user,
$title );
1466 $key = $this->getPageSeenTimestampsKey( $user );
1468 return $this->latestUpdateCache->getWithSetCallback(
1470 BagOStuff::TTL_PROC_LONG,
1471 function () use ( $key ) {
1472 return $this->stash->get( $key ) ?:
null;
1482 return $this->stash->makeGlobalKey(
1483 'watchlist-recent-updates',
1484 $this->lbFactory->getLocalDomainID(),
1494 return "{$target->getNamespace()}:{$target->getDBkey()}";
1513 $oldRev = $this->revisionLookup->getRevisionById( $oldid );
1519 $nextRev = $this->revisionLookup->getNextRevision( $oldRev );
1525 if ( $item ===
null ) {
1526 $item = $this->loadWatchedItem( $user,
$title );
1536 $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
1543 $ts->timestamp->add(
new DateInterval(
'PT1S' ) );
1544 $notificationTimestamp = $ts->getTimestamp( TS_MW );
1546 if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
1547 if ( $force !=
'force' ) {
1551 return $item->getNotificationTimestamp();
1555 return $notificationTimestamp;
1568 if ( $unreadLimit !==
null ) {
1569 $unreadLimit = (int)$unreadLimit;
1570 $queryOptions[
'LIMIT'] = $unreadLimit;
1574 'wl_user' => $user->
getId(),
1575 'wl_notificationtimestamp IS NOT NULL'
1578 $rowCount =
$dbr->selectRowCount(
'watchlist',
'1', $conds, __METHOD__, $queryOptions );
1580 if ( $unreadLimit ===
null ) {
1584 if ( $rowCount >= $unreadLimit ) {
1598 $this->duplicateEntry(
1599 $this->nsInfo->getSubjectPage( $oldTarget ),
1600 $this->nsInfo->getSubjectPage( $newTarget )
1602 $this->duplicateEntry(
1603 $this->nsInfo->getTalkPage( $oldTarget ),
1604 $this->nsInfo->getTalkPage( $newTarget )
1614 $dbw = $this->getConnectionRef(
DB_MASTER );
1615 $result = $this->fetchWatchedItemsForPage( $dbw, $oldTarget );
1617 $newDBkey = $newTarget->
getDBkey();
1619 # Construct array to replace into the watchlist
1622 foreach ( $result as $row ) {
1624 'wl_user' => $row->wl_user,
1625 'wl_namespace' => $newNamespace,
1626 'wl_title' => $newDBkey,
1627 'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
1630 if ( $this->expiryEnabled && $row->we_expiry ) {
1631 $expiries[$row->wl_user] = $row->we_expiry;
1635 if ( empty( $values ) ) {
1644 [ [
'wl_user',
'wl_namespace',
'wl_title' ] ],
1649 if ( $this->expiryEnabled ) {
1650 $this->updateExpiriesAfterMove( $dbw, $expiries, $newNamespace, $newDBkey );
1663 $tables = [
'watchlist' ];
1664 $fields = [
'wl_user',
'wl_notificationtimestamp' ];
1667 if ( $this->expiryEnabled ) {
1668 $tables[] =
'watchlist_expiry';
1669 $fields[] =
'we_expiry';
1670 $joins[
'watchlist_expiry'] = [
'LEFT JOIN', [
'wl_id = we_item' ] ];
1698 $method = __METHOD__;
1700 function () use ( $dbw, $expiries, $namespace, $dbKey, $method ) {
1704 [
'wl_user',
'wl_id' ],
1706 'wl_namespace' => $namespace,
1707 'wl_title' => $dbKey,
1714 foreach (
$res as $row ) {
1715 if ( !empty( $expiries[$row->wl_user] ) ) {
1717 'we_item' => $row->wl_id,
1718 'we_expiry' => $expiries[$row->wl_user],
1724 $batches = array_chunk( $expiryData, $this->updateRowsPerQuery );
1725 foreach ( $batches as $toInsert ) {
1734 DeferredUpdates::POSTSEND,
1745 foreach ( $titles as
$title ) {
1747 $rows[
$title->getNamespace() ][] =
$title->getDBkey();
1757 foreach ( $titles as
$title ) {
1758 $this->uncache( $user,
$title );
1767 return $dbr->selectRowCount(
1770 [
'we_expiry <= ' .
$dbr->addQuotes(
$dbr->timestamp() ) ],
1778 public function removeExpired(
int $limit,
bool $deleteOrphans =
false ): void {
1780 $dbw = $this->getConnectionRef(
DB_MASTER );
1781 $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1784 $toDelete =
$dbr->selectFieldValues(
1787 [
'we_expiry <= ' .
$dbr->addQuotes(
$dbr->timestamp() ) ],
1789 [
'LIMIT' => $limit ]
1791 if ( count( $toDelete ) > 0 ) {
1795 [
'wl_id' => $toDelete ],
1800 [
'we_item' => $toDelete ],
1807 if ( $deleteOrphans ) {
1808 $expiryToDelete =
$dbr->selectFieldValues(
1809 [
'watchlist_expiry',
'watchlist' ],
1812 [
'wl_id' =>
null,
'we_expiry' =>
null ],
1817 [
'watchlist' => [
'LEFT JOIN',
'wl_id = we_item' ] ]
1819 if ( count( $expiryToDelete ) > 0 ) {
1822 [
'we_item' => $expiryToDelete ],
1828 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );