Gergő Tisza has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/320325

Change subject: Add cache layer to the service
......................................................................

Add cache layer to the service

Change-Id: Ib8feb757caf1f19c0b245fd90aec42537b0a84a7
---
M extension.json
M i18n/en.json
M i18n/qqq.json
A includes/CachedPageViewService.php
M includes/Hooks.php
M includes/ServiceWiring.php
A tests/phpunit/CachedPageViewServiceTest.php
7 files changed, 654 insertions(+), 37 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/PageViewInfo 
refs/changes/25/320325/1

diff --git a/extension.json b/extension.json
index 8a24ff3..923ef1c 100644
--- a/extension.json
+++ b/extension.json
@@ -14,6 +14,7 @@
        "AutoloadClasses": {
                "MediaWiki\\Extensions\\PageViewInfo\\Hooks": 
"includes/Hooks.php",
                "MediaWiki\\Extensions\\PageViewInfo\\PageViewService": 
"includes/PageViewService.php",
+               "MediaWiki\\Extensions\\PageViewInfo\\CachedPageViewService": 
"includes/CachedPageViewService.php",
                
"MediaWiki\\Extensions\\PageViewInfo\\WikimediaPageViewService": 
"includes/WikimediaPageViewService.php"
        },
        "MessagesDirs": {
diff --git a/i18n/en.json b/i18n/en.json
index 339d0b0..02f836c 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -8,5 +8,7 @@
        "pvi-month-count": "Page views in the past 30 days",
        "pvi-close": "Close",
        "pvi-range": "$1 - $2",
-       "pvi-invalidresponse": "Invalid response"
+       "pvi-invalidresponse": "Invalid response",
+       "pvi-cached-error": "An earlier attempt to fetch this data failed. To 
limit server load, retries have been blocked for $1.",
+       "pvi-cached-error-title": "An earlier attempt to fetch page \"$1\" 
failed. To limit server load, retries have been blocked for $2."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index ba260de..6a186c0 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -9,5 +9,7 @@
        "pvi-month-count": "Label for table cell containing page views in past 
30 days",
        "pvi-close": "Text on button to close a dialog\n{{Identical|Close}}",
        "pvi-range": "Title of dialog, which is the date range the graph is 
for. $1 is the starting date, $2 is the ending date.",
-       "pvi-invalidresponse": "Error message when the REST API response data 
does not have the expected structure."
+       "pvi-invalidresponse": "Error message when the REST API response data 
does not have the expected structure.",
+       "pvi-cached-error": "Shown when the cached result is an error. $1 is 
the retry delay in human-readable form (e.g. \"30 minutes\").",
+       "pvi-cached-error-title": "Shown when the cached result (which is part 
of a larger resultset) is an error. $1 is the page name, $2 the retry delay in 
human-readable form (e.g. \"30 minutes\")."
 }
diff --git a/includes/CachedPageViewService.php 
b/includes/CachedPageViewService.php
new file mode 100644
index 0000000..b3fb540
--- /dev/null
+++ b/includes/CachedPageViewService.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace MediaWiki\Extensions\PageViewInfo;
+
+use BagOStuff;
+use InvalidArgumentException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Status;
+use StatusValue;
+use Title;
+
+/**
+ * Wraps a PageViewService and caches the results.
+ */
+class CachedPageViewService implements PageViewService, LoggerAwareInterface {
+       const ERROR_EXPIRY = 1800;
+
+       /** @var PageViewService */
+       protected $service;
+
+       /** @var BagOStuff */
+       protected $cache;
+
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var string Cache prefix, in case multiple instances of this 
service coexist */
+       protected $prefix;
+
+       /** @var int */
+       protected $cachedDays = 30;
+
+       public function __construct( PageViewService $service, BagOStuff 
$cache, $prefix = null ) {
+               $this->service = $service;
+               $this->logger = new NullLogger();
+               $this->cache = $cache;
+               $this->prefix = $prefix;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Set the number of days that will be cached. To avoid cache 
fragmentation, the inner service
+        * is always called with this number of days; if necessary, the 
response will be expanded with
+        * nulls.
+        * @param int $cachedDays
+        */
+       public function setCachedDays( $cachedDays ) {
+               $this->cachedDays = $cachedDays;
+       }
+
+       public function supports( $metric, $scope ) {
+               return $this->service->supports( $metric, $scope );
+       }
+
+       public function getPageData( array $titles, $days, $metric = 
self::METRIC_VIEW ) {
+               $status = $this->getTitlesWithCache( $metric, $titles );
+               $data = $status->getValue();
+               foreach ( $data as $title => $titleData ) {
+                       if ( $days < $this->cachedDays ) {
+                               $data[$title] = array_slice( $titleData, 
-$days, null, true );
+                       } elseif ( $days > $this->cachedDays ) {
+                               $data[$title] = $this->extendDateRange( 
$titleData, $days );
+                       }
+               }
+               $status->setResult( $status->isOK(), $data );
+               return $status;
+       }
+
+       public function getSiteData( $days, $metric = self::METRIC_VIEW ) {
+               $status = $this->getWithCache( $metric, self::SCOPE_SITE );
+               if ( $status->isOK() ) {
+                       $data = $status->getValue();
+                       if ( $days < $this->cachedDays ) {
+                               $data = array_slice( $data, -$days, null, true 
);
+                       } elseif ( $days > $this->cachedDays ) {
+                               $data = $this->extendDateRange( $data, $days );
+                       }
+                       $status->setResult( true, $data );
+               }
+               return $status;
+       }
+
+       public function getTopPages( $metric = self::METRIC_VIEW ) {
+               return $this->getWithCache( $metric, self::SCOPE_TOP );
+       }
+
+       public function getCacheExpiry( $metric, $scope ) {
+               // add some random delay to avoid cache stampedes
+               return $this->service->getCacheExpiry( $metric, $scope ) + 
mt_rand( 0, 600 );
+       }
+
+       /**
+        * Like BagOStuff::getWithSetCallback, but returns a StatusValue like 
PageViewService calls do.
+        * Returns (and caches) null wrapped in a StatusValue on error.
+        * @param string $metric A METRIC_* constant
+        * @param string $scope A SCOPE_* constant (except SCOPE_ARTICLE which 
has its own method)
+        * @return StatusValue
+        */
+       protected function getWithCache( $metric, $scope ) {
+               $key = $this->cache->makeKey( 'pvi', $this->prefix, ( $scope 
=== self::SCOPE_SITE ) ?
+                       $this->cachedDays : null, $metric, $scope );
+               $data = $this->cache->get( $key );
+               if ( $data === false ) { // no cached data
+                       /** @var StatusValue $status */
+                       switch ( $scope ) {
+                               case self::SCOPE_SITE:
+                                       $status = $this->service->getSiteData( 
$this->cachedDays, $metric );
+                                       break;
+                               case self::SCOPE_TOP:
+                                       $status = $this->service->getTopPages( 
$metric );
+                                       break;
+                               default:
+                                       throw new InvalidArgumentException( 
"invalid scope: $scope" );
+                       }
+                       if ( $status->isOK() ) {
+                               $data = $status->getValue();
+                               $expiry = $this->getCacheExpiry( $metric, 
$scope );
+                       } else {
+                               $data = null;
+                               $expiry = self::ERROR_EXPIRY;
+                       }
+                       $this->cache->set( $key, $data, $expiry );
+               } elseif ( $data === null ) { // cached error
+                       $status = StatusValue::newGood( [] );
+                       $status->fatal( 'pvi-cached-error', 
\Message::durationParam( self::ERROR_EXPIRY ) );
+               } else { // valid cached data
+                       $status = StatusValue::newGood( $data );
+               }
+               return $status;
+       }
+
+       /**
+        * The equivalent of getWithCache for multiple titles (ie. for 
SCOPE_ARTICLE).
+        * Errors are also handled per-article.
+        * @param string $metric A METRIC_* constant
+        * @param Title[] $titles
+        * @return StatusValue
+        */
+       protected function getTitlesWithCache( $metric, array $titles = null ) {
+               // Set up the response array, without any values. This will 
help preserve the order of titles.
+               $data = array_fill_keys( array_map( function ( Title $t ) {
+                       return $t->getPrefixedDBkey();
+               }, $titles ), false );
+
+               // Fetch data for all titles from cache. Hopefully we are using 
a cache which has
+               // a cheap getMulti implementation.
+               $titleToCacheKey = $statuses = [];
+               foreach ( $titles as $title ) {
+                       $titleToCacheKey[$title->getPrefixedDBkey()] = 
$this->cache->makeKey( 'pvi', $this->prefix,
+                               $this->cachedDays, $metric, 
self::SCOPE_ARTICLE, md5( $title->getPrefixedDBkey() ) );
+               }
+               $cacheKeyToTitle = array_flip( $titleToCacheKey );
+               $rawData = $this->cache->getMulti( array_keys( $cacheKeyToTitle 
) );
+               foreach ( $rawData as $key => $value ) {
+                       // BagOStuff::getMulti is unclear on how missing items 
should be handled; let's
+                       // assume some implementations might return that key 
with a value of false
+                       if ( $value !== false ) {
+                               $statuses[$cacheKeyToTitle[$key]] = empty( 
$value['#error'] ) ? StatusValue::newGood()
+                                       : StatusValue::newFatal( 
'pvi-cached-error-title', $cacheKeyToTitle[$key],
+                                               \Message::durationParam( 
self::ERROR_EXPIRY ) );
+                               unset( $value['#error'] );
+                               $data[$cacheKeyToTitle[$key]] = $value;
+                       }
+               }
+
+               // Now get and cache the data for the remaining titles from the 
real service. It might not
+               // return data for all of them.
+               foreach ( $titles as $i => $title ) {
+                       if ( $data[$title->getPrefixedDBkey()] !== false ) {
+                               unset( $titles[$i] );
+                       }
+               }
+               $uncachedStatus = $this->service->getPageData( $titles, 
$this->cachedDays, $metric );
+               foreach ( $uncachedStatus->success as $title => $succes ) {
+                       $titleData = isset( $uncachedStatus->getValue()[$title] 
) ?
+                               $uncachedStatus->getValue()[$title] : null;
+                       if ( !is_array( $titleData ) || count( $titleData ) < 
$this->cachedDays ) {
+                               // PageViewService is expected to return [ date 
=> null ] for all requested dates
+                               $this->logger->warning( 'Upstream service 
returned invalid data for {title}', [
+                                       'title' => $title,
+                                       'statusMessage' => Status::wrap( 
$uncachedStatus )->getWikiText( null, null, 'en' ),
+                               ] );
+                               $titleData = $this->extendDateRange( is_array( 
$titleData ) ? $titleData : [],
+                                       $this->cachedDays );
+                       }
+                       $data[$title] = $titleData;
+                       if ( $succes ) {
+                               $statuses[$title] = StatusValue::newGood();
+                               $expiry = $this->getCacheExpiry( $metric, 
self::SCOPE_ARTICLE );
+                       } else {
+                               $data[$title]['#error'] = true;
+                               $statuses[$title] = StatusValue::newFatal( 
'pvi-cached-error-title', $title,
+                                       \Message::durationParam( 
self::ERROR_EXPIRY ) );
+                               $expiry = self::ERROR_EXPIRY;
+                       }
+                       $this->cache->set( $titleToCacheKey[$title], 
$data[$title], $expiry );
+                       unset( $data[$title]['#error'] );
+               }
+
+               // Almost done; we need to truncate the data at the first 
"hole" (title not returned
+               // either by getMulti or getPageData) so we return a 
consecutive prefix of the
+               // requested titles and do not mess up continuation.
+               $holeIndex = array_search( false, array_values( $data ), true );
+               $data = array_slice( $data, 0, $holeIndex ?: null, true );
+               $statuses = array_slice( $statuses, 0, $holeIndex ?: null, true 
);
+
+               $status = StatusValue::newGood( $data );
+               array_walk( $statuses, [ $status, 'merge' ] );
+               $status->success = array_map( function( StatusValue $s ) {
+                        return $s->isOK();
+               }, $statuses );
+               $status->successCount = count( array_filter( $status->success ) 
);
+               $status->failCount = count( $status->success ) - 
$status->successCount;
+               $status->setResult( array_filter( $status->success ), $data );
+               return $status;
+       }
+
+       /**
+        * Add extra days (with a null value) to the beginning of a date range 
to make it have at least
+        * ::$cachedDays days.
+        * @param array $data YYYY-MM-DD => count, ordered, has less than 
$cachedDays items
+        * @param int $days
+        * @return array
+        */
+       protected function extendDateRange( $data, $days ) {
+               reset( $data );
+               // set to noon to avoid skip second and similar problems
+               $day = strtotime( key( $data ) . 'T00:00Z' ) + 12 * 3600;
+               for ( $i = $days - count( $data ); $i > 0; $i-- ) {
+                       $day -= 24 * 3600;
+                       $data = [ gmdate( 'Y-m-d', $day ) => null ] + $data;
+               }
+               return $data;
+       }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index e94ac8c..7ec083e 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -6,20 +6,27 @@
 use FormatJson;
 use Html;
 use MediaWiki\MediaWikiServices;
-use ObjectCache;
-use Title;
 
 class Hooks {
-
        /**
         * @param IContextSource $ctx
         * @param array $pageInfo
         */
        public static function onInfoAction( IContextSource $ctx, array 
&$pageInfo ) {
-               $views = self::getMonthViews( $ctx->getTitle() );
-               if ( !$views ) {
+               /** @var PageViewService $pageViewService */
+               $pageViewService = 
MediaWikiServices::getInstance()->getService( 'PageViewService' );
+               if ( !$pageViewService->supports( PageViewService::METRIC_VIEW,
+                       PageViewService::SCOPE_ARTICLE )
+               ) {
                        return;
                }
+               $title = $ctx->getTitle();
+               $status = $pageViewService->getPageData( [ $title ], 30, 
PageViewService::METRIC_VIEW );
+               $data = $status->getValue();
+               if ( !$status->isOK() ) {
+                       return;
+               }
+               $views = $data[$title->getPrefixedDBkey()];
 
                $total = array_sum( $views );
                reset( $views );
@@ -52,33 +59,6 @@
                                'end' => $lang->userDate( $end, $user ),
                        ],
                ] );
-       }
-
-       protected static function getMonthViews( Title $title ) {
-               $cache = ObjectCache::getLocalClusterInstance();
-               $key = $cache->makeKey( 'pvi', 'month', md5( 
$title->getPrefixedText() ) );
-               $data = $cache->get( $key );
-               if ( $data ) {
-                       return $data;
-               }
-
-               /** @var PageViewService $pageViewService */
-               $pageViewService = 
MediaWikiServices::getInstance()->getService( 'PageViewService' );
-               if ( !$pageViewService->supports( PageViewService::METRIC_VIEW,
-                       PageViewService::SCOPE_ARTICLE )
-               ) {
-                       return false;
-               }
-
-               $status = $pageViewService->getPageData( [ $title ], 30, 
PageViewService::METRIC_VIEW );
-               if ( !$status->isOK() ) {
-                       $cache->set( $key, false, 300 );
-               }
-
-               $data = $status->getValue()[$title->getPrefixedDBkey()];
-               $cache->set( $key, $data, $pageViewService->getCacheExpiry( 
PageViewService::METRIC_VIEW,
-                       PageViewService::SCOPE_ARTICLE ) );
-               return $data;
        }
 
        /**
diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
index cbd86db..981aea9 100644
--- a/includes/ServiceWiring.php
+++ b/includes/ServiceWiring.php
@@ -4,6 +4,7 @@
 
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use ObjectCache;
 
 return [
        'PageViewService' => function ( MediaWikiServices $services ) {
@@ -12,9 +13,14 @@
                $endpoint = $extensionConfig->get( 
'PageViewInfoWikimediaEndpoint' );
                $project = $extensionConfig->get( 'PageViewInfoWikimediaDomain' 
)
                        ?: $mainConfig->get( 'ServerName' );
-               $pageViewService = new WikimediaPageViewService( $endpoint, [ 
'project' => $project ],
+               $cache = ObjectCache::getLocalClusterInstance();
+               $logger = LoggerFactory::getInstance( 'PageViewInfo' );
+
+               $service = new WikimediaPageViewService( $endpoint, [ 'project' 
=> $project ],
                        $extensionConfig->get( 
'PageViewInfoWikimediaRequestLimit' ) );
-               $pageViewService->setLogger( LoggerFactory::getInstance( 
'PageViewInfo' ) );
-               return $pageViewService;
+               $service->setLogger( $logger );
+               $cachedService = new CachedPageViewService( $service, $cache );
+               $cachedService->setLogger( $logger );
+               return $cachedService;
        },
 ];
diff --git a/tests/phpunit/CachedPageViewServiceTest.php 
b/tests/phpunit/CachedPageViewServiceTest.php
new file mode 100644
index 0000000..9772667
--- /dev/null
+++ b/tests/phpunit/CachedPageViewServiceTest.php
@@ -0,0 +1,386 @@
+<?php
+
+namespace MediaWiki\Extensions\PageViewInfo;
+
+use HashBagOStuff;
+use StatusValue;
+
+class CachedPageViewServiceTest extends \PHPUnit_Framework_TestCase {
+       /** @var CachedPageViewService */
+       protected $service;
+       /** @var \PHPUnit_Framework_MockObject_MockObject */
+       protected $mock;
+
+       protected function setUp() {
+               parent::setUp();
+               $cache = new HashBagOStuff();
+               $this->mock = $this->getMockBuilder( PageViewService::class 
)->getMockForAbstractClass();
+               $this->service = new CachedPageViewService( $this->mock, $cache 
);
+               $this->service->setCachedDays( 2 );
+       }
+
+       /**
+        * @dataProvider provideSupports
+        */
+       public function testSupports( $metric, $scope, $expectation ) {
+               $this->mock->expects( $this->once() )
+                       ->method( 'supports' )
+                       ->willReturnCallback( function ( $metric, $scope ) {
+                               return $metric === PageViewService::METRIC_VIEW 
||
+                                          $scope === 
PageViewService::SCOPE_SITE;
+                       } );
+               $this->assertSame( $expectation, $this->service->supports( 
$metric, $scope ) );
+       }
+
+       public function provideSupports() {
+               return [
+                       [ PageViewService::METRIC_VIEW, 
PageViewService::SCOPE_ARTICLE, true ],
+                       [ PageViewService::METRIC_UNIQUE, 
PageViewService::SCOPE_SITE, true ],
+                       [ PageViewService::METRIC_UNIQUE, 
PageViewService::SCOPE_ARTICLE, false ],
+               ];
+       }
+
+       public function testGetCacheExpiry() {
+               $this->mock->expects( $this->once() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $expiry = $this->service->getCacheExpiry( 
PageViewService::METRIC_VIEW,
+                       PageViewService::SCOPE_ARTICLE );
+               $this->assertGreaterThanOrEqual( 1000, $expiry );
+               $this->assertLessThanOrEqual( 1600, $expiry );
+       }
+
+       public function testGetPageData() {
+               $expectedTitles = [];
+               $this->service->setCachedDays( 2 );
+               $this->mock->expects( $this->any() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $this->mock->expects( $this->any() )
+                       ->method( 'getPageData' )
+                       ->with( $this->anything(), $this->anything(), 
$this->logicalOr(
+                               PageViewService::METRIC_VIEW, 
PageViewService::METRIC_UNIQUE ) )
+                       ->willReturnCallback( function ( $titles, $days, 
$metric ) use ( &$expectedTitles ) {
+                               $metric = ( $metric === 
PageViewService::METRIC_VIEW ) + 1;
+                               $titles = array_fill_keys( array_map( function 
( \Title $t ) {
+                                       return $t->getPrefixedDBkey();
+                               }, $titles ), null );
+                               $this->assertSame( $expectedTitles, array_keys( 
$titles ) );
+                               // 'A' => 1, 'B' => 2, ...
+                               $pages = array_combine( array_map( function ( 
$n ) {
+                                       return chr( ord( 'A' ) + $n - 1 );
+                               }, range( 1, 20 ) ), range( 1, 20 ) );
+                               // simulate a page-per-query limit of 3
+                               $base = array_slice( array_intersect_key( 
$pages, $titles ), 0, 3 );
+                               $perDay = array_slice( [
+                                       '2000-01-01' => $metric * 1,
+                                       '2000-01-02' => $metric * 2,
+                                       '2000-01-03' => $metric * 3,
+                                       '2000-01-04' => $metric * 4,
+                                       '2000-01-05' => $metric * 5,
+                               ], -$days );
+                               // add some errors
+                               $data = array_map( function ( $multiplier ) use 
( $perDay ) {
+                                       if ( $multiplier > 10 && $multiplier % 
2 ) {
+                                               return null;
+                                       }
+                                       return array_map( function ( $count ) 
use ( $multiplier ) {
+                                               return $count * $multiplier;
+                                       }, $perDay );
+                               }, $base );
+                               $status = StatusValue::newGood();
+                               foreach ( $data as $title => $titleData ) {
+                                       if ( $titleData === null ) {
+                                               $status->error( '500 #' . 
$title );
+                                       }
+                               }
+                               $status->success = array_map( function ( $v ) {
+                                       return $v !== null;
+                               }, $data );
+                               $status->successCount = count( array_filter( 
$status->success ) );
+                               $status->failCount = count( $status->success ) 
- $status->successCount;
+                               $status->setResult( $status->successCount, 
array_map( function ( $titleData ) use ( $perDay ) {
+                                       return $titleData ?: array_fill_keys( 
array_keys( $perDay ), null );
+                               }, $data ) );
+                               return $status;
+                       } );
+               $makeTitles = function( $titles ) {
+                       return array_map( function ( $t ) {
+                               return \Title::newFromText( $t );
+                       }, $titles );
+               };
+
+               $expectedTitles = [ 'A', 'B' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'B' 
] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 10 ],
+                       'B' => [ '2000-01-05' => 20 ],
+               ], $status->getValue() );
+               $this->assertSame( [ 'A' => true, 'B' => true ], 
$status->success );
+               $this->assertSame( 2, $status->successCount );
+               $this->assertSame( 0, $status->failCount );
+
+               // second call should not trigger a new call to the wrapped 
service, regardless of $days
+               // days beyond setCachedDays() should be null
+               $expectedTitles = [];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'B' 
] ), 3,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-03' => null, '2000-01-04' => 8, 
'2000-01-05' => 10 ],
+                       'B' => [ '2000-01-03' => null, '2000-01-04' => 16, 
'2000-01-05' => 20 ],
+               ], $status->getValue() );
+               $this->assertSame( [ 'A' => true, 'B' => true ], 
$status->success );
+               $this->assertSame( 2, $status->successCount );
+               $this->assertSame( 0, $status->failCount );
+
+               // caching should not mix up metrics
+               $expectedTitles = [ 'A', 'B' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'B' 
] ), 1,
+                       PageViewService::METRIC_UNIQUE );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 5 ],
+                       'B' => [ '2000-01-05' => 10 ],
+               ], $status->getValue() );
+               $this->assertSame( [ 'A' => true, 'B' => true ], 
$status->success );
+               $this->assertSame( 2, $status->successCount );
+               $this->assertSame( 0, $status->failCount );
+
+               // titles should be cached individually
+               $expectedTitles = [ 'C' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'C' 
] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 10 ],
+                       'C' => [ '2000-01-05' => 30 ],
+               ], $status->getValue() );
+               $this->assertSame( [ 'A' => true, 'C' => true ], 
$status->success );
+               $this->assertSame( 2, $status->successCount );
+               $this->assertSame( 0, $status->failCount );
+
+               // needs to handle the wrapped service returning less pages 
than asked
+               $expectedTitles = [ 'D', 'E', 'F', 'G' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'B', 
'C', 'D', 'E', 'F', 'G' ] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 10 ],
+                       'B' => [ '2000-01-05' => 20 ],
+                       'C' => [ '2000-01-05' => 30 ],
+                       'D' => [ '2000-01-05' => 40 ],
+                       'E' => [ '2000-01-05' => 50 ],
+                       'F' => [ '2000-01-05' => 60 ],
+               ], $status->getValue() );
+               $this->assertSame( [ 'A' => true, 'B' => true, 'C' => true, 'D' 
=> true, 'E' => true,
+                       'F' => true ], $status->success );
+               $this->assertSame( 6, $status->successCount );
+               $this->assertSame( 0, $status->failCount );
+
+               // some errors
+               $expectedTitles = [ 'K', 'L' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'K', 
'L' ] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertFalse( $status->isGood() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 10 ],
+                       'K' => [ '2000-01-05' => null ],
+                       'L' => [ '2000-01-05' => 120 ],
+               ], $status->getValue() );
+               $this->assertTrue( $status->hasMessage( 
'pvi-cached-error-title' ) );
+               $this->assertFalse( $status->hasMessage( '500 #K' ) );
+               $this->assertSame( [ 'A' => true, 'K' => false, 'L' => true ], 
$status->success );
+               $this->assertSame( 2, $status->successCount );
+               $this->assertSame( 1, $status->failCount );
+
+               // cached error
+               $expectedTitles = [ 'N' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'A', 'K', 
'L', 'N' ] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertFalse( $status->isGood() );
+               $this->assertSame( [
+                       'A' => [ '2000-01-05' => 10 ],
+                       'K' => [ '2000-01-05' => null ],
+                       'L' => [ '2000-01-05' => 120 ],
+                       'N' => [ '2000-01-05' => 140 ],
+               ], $status->getValue() );
+               $this->assertTrue( $status->hasMessage( 
'pvi-cached-error-title' ) );
+               $this->assertSame( [ 'A' => true, 'K' => false, 'L' => true, 
'N' => true ], $status->success );
+               $this->assertSame( 3, $status->successCount );
+               $this->assertSame( 1, $status->failCount );
+
+               // all errors
+               $expectedTitles = [ 'M' ];
+               $status = $this->service->getPageData( $makeTitles( [ 'K', 'M' 
] ), 1,
+                       PageViewService::METRIC_VIEW );
+               $this->assertFalse( $status->isOK() );
+               $this->assertFalse( $status->isGood() );
+               $this->assertSame( [
+                       'K' => [ '2000-01-05' => null ],
+                       'M' => [ '2000-01-05' => null ],
+               ], $status->getValue() );
+               $this->assertTrue( $status->hasMessage( 
'pvi-cached-error-title' ) );
+               $this->assertFalse( $status->hasMessage( '500 #M' ) );
+               $this->assertSame( [ 'K' => false, 'M' => false ], 
$status->success );
+               $this->assertSame( 0, $status->successCount );
+               $this->assertSame( 2, $status->failCount );
+       }
+
+       public function testGetSiteData() {
+               $cached = false;
+               $this->service->setCachedDays( 2 );
+               $this->mock->expects( $this->any() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $this->mock->expects( $this->exactly( 2 ) )
+                       ->method( 'getSiteData' )
+                       ->with( $this->anything(), $this->logicalOr( 
PageViewService::METRIC_VIEW,
+                               PageViewService::METRIC_UNIQUE ) )
+                       ->willReturnCallback( function ( $days, $metric ) use ( 
&$cached ) {
+                               if ( $cached ) {
+                                       $this->fail( 'should have been cached' 
);
+                               }
+                               $metric = ( $metric === 
PageViewService::METRIC_VIEW ) + 1;
+                               return StatusValue::newGood( array_slice( [
+                                       '2000-01-01' => $metric * 1,
+                                       '2000-01-02' => $metric * 2,
+                                       '2000-01-03' => $metric * 3,
+                                       '2000-01-04' => $metric * 4,
+                                       '2000-01-05' => $metric * 5,
+                               ], -$days ) );
+                       } );
+               $status = $this->service->getSiteData( 1, 
PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ '2000-01-05' => 10 ], $status->getValue() 
);
+
+               // second call should not trigger a new call to the wrapped 
service, regardless of $days
+               // days beyond setCachedDays() should be null
+               $cached = true;
+               $status = $this->service->getSiteData( 3, 
PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ '2000-01-03' => null, '2000-01-04' => 8, 
'2000-01-05' => 10 ],
+                       $status->getValue() );
+
+               // caching should not mix up metrics
+               $cached = false;
+               $status = $this->service->getSiteData( 1, 
PageViewService::METRIC_UNIQUE );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ '2000-01-05' => 5 ], $status->getValue() );
+       }
+
+       public function testGetSiteData_error() {
+               $cached = false;
+               $this->service->setCachedDays( 2 );
+               $this->mock->expects( $this->any() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $this->mock->expects( $this->exactly( 2 ) )
+                       ->method( 'getSiteData' )
+                       ->with( $this->anything(), $this->logicalOr( 
PageViewService::METRIC_VIEW,
+                               PageViewService::METRIC_UNIQUE ) )
+                       ->willReturnCallback( function ( $days, $metric ) use ( 
&$cached ) {
+                               if ( $cached ) {
+                                       $this->fail( 'should have been cached' 
);
+                               }
+                               return $metric === PageViewService::METRIC_VIEW 
? StatusValue::newFatal( '500' )
+                                       : StatusValue::newFatal( '500 #2' );
+                       } );
+               $status = $this->service->getSiteData( 1, 
PageViewService::METRIC_VIEW );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( '500' ) );
+               $this->assertFalse( $status->hasMessage( 'pvi-cached-error' ) );
+
+               // second call should not trigger a new call to the wrapped 
service, regardless of $days
+               $cached = true;
+               $status = $this->service->getSiteData( 3, 
PageViewService::METRIC_VIEW );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( 'pvi-cached-error' ) );
+
+               // caching should not mix up metrics
+               $cached = false;
+               $status = $this->service->getSiteData( 1, 
PageViewService::METRIC_UNIQUE );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( '500 #2' ) );
+               $this->assertFalse( $status->hasMessage( 'pvi-cached-error' ) );
+       }
+
+       public function testGetTopPages() {
+               $cached = false;
+               $this->mock->expects( $this->any() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $this->mock->expects( $this->exactly( 2 ) )
+                       ->method( 'getTopPages' )
+                       ->with( $this->logicalOr( PageViewService::METRIC_VIEW, 
PageViewService::METRIC_UNIQUE ) )
+                       ->willReturnCallback( function ( $metric ) use ( 
&$cached ) {
+                               if ( $cached ) {
+                                       $this->fail( 'should have been cached' 
);
+                               }
+                               switch ( $metric ) {
+                                       case PageViewService::METRIC_VIEW:
+                                               return StatusValue::newGood( [ 
'A' => 100, 'B' => 10, 'C' => 1 ] );
+                                       case PageViewService::METRIC_UNIQUE:
+                                               return StatusValue::newGood( [ 
'A' => 50, 'B' => 5, 'C' => 1 ] );
+                               }
+                       } );
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ 'A' => 100, 'B' => 10, 'C' => 1 ], 
$status->getValue() );
+
+               // second call should not trigger a new call to the wrapped 
service
+               $cached = true;
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_VIEW );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ 'A' => 100, 'B' => 10, 'C' => 1 ], 
$status->getValue() );
+
+               // caching should not mix up metrics
+               $cached = false;
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_UNIQUE );
+               $this->assertTrue( $status->isOK() );
+               $this->assertSame( [ 'A' => 50, 'B' => 5, 'C' => 1 ], 
$status->getValue() );
+       }
+
+       public function testGetTopPages_error() {
+               $cached = false;
+               $this->mock->expects( $this->any() )
+                       ->method( 'getCacheExpiry' )
+                       ->willReturn( 1000 );
+               $this->mock->expects( $this->exactly( 2 ) )
+                       ->method( 'getTopPages' )
+                       ->with( $this->logicalOr( PageViewService::METRIC_VIEW, 
PageViewService::METRIC_UNIQUE ) )
+                       ->willReturnCallback( function ( $metric ) use ( 
&$cached ) {
+                               if ( $cached ) {
+                                       $this->fail( 'should have been cached' 
);
+                               }
+                               switch ( $metric ) {
+                                       case PageViewService::METRIC_VIEW:
+                                               return StatusValue::newFatal( 
'500' );
+                                       case PageViewService::METRIC_UNIQUE:
+                                               return StatusValue::newFatal( 
'500 #2' );
+                               }
+                       } );
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_VIEW );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( '500' ) );
+               $this->assertFalse( $status->hasMessage( 'pvi-cached-error' ) );
+
+               // second call should not trigger a new call to the wrapped 
service
+               $cached = true;
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_VIEW );
+               $this->assertFalse( $status->isOK() );
+               $this->assertFalse( $status->hasMessage( '500' ) );
+               $this->assertTrue( $status->hasMessage( 'pvi-cached-error' ) );
+
+               // caching should not mix up metrics
+               $cached = false;
+               $status = $this->service->getTopPages( 
PageViewService::METRIC_UNIQUE );
+               $this->assertFalse( $status->isOK() );
+               $this->assertTrue( $status->hasMessage( '500 #2' ) );
+               $this->assertFalse( $status->hasMessage( 'pvi-cached-error' ) );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/320325
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib8feb757caf1f19c0b245fd90aec42537b0a84a7
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/PageViewInfo
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to