Introduce StatsdAwareInterface
[lhc/web/wiklou.git] / includes / WatchedItemStore.php
index 4e2dfa5..8ae7932 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use Wikimedia\Assert\Assert;
 
 /**
@@ -10,7 +11,10 @@ use Wikimedia\Assert\Assert;
  *
  * @since 1.27
  */
-class WatchedItemStore {
+class WatchedItemStore implements StatsdAwareInterface {
+
+       const SORT_DESC = 'DESC';
+       const SORT_ASC = 'ASC';
 
        /**
         * @var LoadBalancer
@@ -40,6 +44,11 @@ class WatchedItemStore {
         */
        private $revisionGetTimestampFromIdCallback;
 
+       /**
+        * @var StatsdDataFactoryInterface
+        */
+       private $stats;
+
        /**
         * @var self|null
         */
@@ -55,10 +64,15 @@ class WatchedItemStore {
        ) {
                $this->loadBalancer = $loadBalancer;
                $this->cache = $cache;
+               $this->stats = new NullStatsdDataFactory();
                $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
                $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
        }
 
+       public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
+               $this->stats = $stats;
+       }
+
        /**
         * Overrides the DeferredUpdates::addCallableUpdate callback
         * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
@@ -145,6 +159,7 @@ class WatchedItemStore {
                                wfGetLB(),
                                new HashBagOStuff( [ 'maxKeys' => 100 ] )
                        );
+                       self::$instance->setStatsdDataFactory( RequestContext::getMain()->getStats() );
                }
                return self::$instance;
        }
@@ -163,18 +178,22 @@ class WatchedItemStore {
                $key = $this->getCacheKey( $user, $target );
                $this->cache->set( $key, $item );
                $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
+               $this->stats->increment( 'WatchedItemStore.cache' );
        }
 
        private function uncache( User $user, LinkTarget $target ) {
                $this->cache->delete( $this->getCacheKey( $user, $target ) );
                unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
+               $this->stats->increment( 'WatchedItemStore.uncache' );
        }
 
        private function uncacheLinkTarget( LinkTarget $target ) {
                if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
                        return;
                }
+               $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
                foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
+                       $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
                        $this->cache->delete( $key );
                }
        }
@@ -448,8 +467,10 @@ class WatchedItemStore {
 
                $cached = $this->getCached( $user, $target );
                if ( $cached ) {
+                       $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
                        return $cached;
                }
+               $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
                return $this->loadWatchedItem( $user, $target );
        }
 
@@ -490,6 +511,54 @@ class WatchedItemStore {
                return $item;
        }
 
+       /**
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'forWrite' => bool defaults to false
+        *        'sort' => string optional sorting by namespace ID and title
+        *                     one of the self::SORT_* constants
+        *
+        * @return WatchedItem[]
+        */
+       public function getWatchedItemsForUser( User $user, array $options = [] ) {
+               $options += [ 'forWrite' => false ];
+
+               $dbOptions = [];
+               if ( array_key_exists( 'sort', $options ) ) {
+                       Assert::parameter(
+                               ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
+                               '$options[\'sort\']',
+                               'must be SORT_ASC or SORT_DESC'
+                       );
+                       $dbOptions['ORDER BY'] = [
+                               "wl_namespace {$options['sort']}",
+                               "wl_title {$options['sort']}"
+                       ];
+               }
+               $db = $this->getConnection( $options['forWrite'] ? DB_MASTER : DB_SLAVE );
+
+               $res = $db->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                       [ 'wl_user' => $user->getId() ],
+                       __METHOD__,
+                       $dbOptions
+               );
+               $this->reuseConnection( $db );
+
+               $watchedItems = [];
+               foreach ( $res as $row ) {
+                       // todo these could all be cached at some point?
+                       $watchedItems[] = new WatchedItem(
+                               $user,
+                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
+                               $row->wl_notificationtimestamp
+                       );
+               }
+
+               return $watchedItems;
+       }
+
        /**
         * Must be called separately for Subject & Talk namespaces
         *
@@ -502,6 +571,61 @@ class WatchedItemStore {
                return (bool)$this->getWatchedItem( $user, $target );
        }
 
+       /**
+        * @param User $user
+        * @param LinkTarget[] $targets
+        *
+        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
+        *         where $timestamp is:
+        *         - string|null value of wl_notificationtimestamp,
+        *         - false if $target is not watched by $user.
+        */
+       public function getNotificationTimestampsBatch( User $user, array $targets ) {
+               $timestamps = [];
+               foreach ( $targets as $target ) {
+                       $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
+               }
+
+               if ( $user->isAnon() ) {
+                       return $timestamps;
+               }
+
+               $targetsToLoad = [];
+               foreach ( $targets as $target ) {
+                       $cachedItem = $this->getCached( $user, $target );
+                       if ( $cachedItem ) {
+                               $timestamps[$target->getNamespace()][$target->getDBkey()] =
+                                       $cachedItem->getNotificationTimestamp();
+                       } else {
+                               $targetsToLoad[] = $target;
+                       }
+               }
+
+               if ( !$targetsToLoad ) {
+                       return $timestamps;
+               }
+
+               $dbr = $this->getConnection( DB_SLAVE );
+
+               $lb = new LinkBatch( $targetsToLoad );
+               $res = $dbr->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                       [
+                               $lb->constructSet( 'wl', $dbr ),
+                               'wl_user' => $user->getId(),
+                       ],
+                       __METHOD__
+               );
+               $this->reuseConnection( $dbr );
+
+               foreach ( $res as $row ) {
+                       $timestamps[(int)$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp;
+               }
+
+               return $timestamps;
+       }
+
        /**
         * Must be called separately for Subject & Talk namespaces
         *
@@ -509,30 +633,30 @@ class WatchedItemStore {
         * @param LinkTarget $target
         */
        public function addWatch( User $user, LinkTarget $target ) {
-               $this->addWatchBatch( [ [ $user, $target ] ] );
+               $this->addWatchBatchForUser( $user, [ $target ] );
        }
 
        /**
-        * @param array[] $userTargetCombinations array of arrays containing [0] => User [1] => LinkTarget
+        * @param User $user
+        * @param LinkTarget[] $targets
         *
         * @return bool success
         */
-       public function addWatchBatch( array $userTargetCombinations ) {
+       public function addWatchBatchForUser( User $user, array $targets ) {
                if ( $this->loadBalancer->getReadOnlyReason() !== false ) {
                        return false;
                }
+               // Only loggedin user can have a watchlist
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               if ( !$targets ) {
+                       return true;
+               }
 
                $rows = [];
-               foreach ( $userTargetCombinations as list( $user, $target ) ) {
-                       /**
-                        * @var User $user
-                        * @var LinkTarget $target
-                        */
-
-                       // Only loggedin user can have a watchlist
-                       if ( $user->isAnon() ) {
-                               continue;
-                       }
+               foreach ( $targets as $target ) {
                        $rows[] = [
                                'wl_user' => $user->getId(),
                                'wl_namespace' => $target->getNamespace(),
@@ -542,10 +666,6 @@ class WatchedItemStore {
                        $this->uncache( $user, $target );
                }
 
-               if ( !$rows ) {
-                       return false;
-               }
-
                $dbw = $this->getConnection( DB_MASTER );
                foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
                        // Use INSERT IGNORE to avoid overwriting the notification timestamp