jenkins-bot has submitted this change and it was merged. (
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(-)
Approvals:
jenkins-bot: Verified
Anomie: Looks good to me, approved
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: merged
Gerrit-Change-Id: Ib8feb757caf1f19c0b245fd90aec42537b0a84a7
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/PageViewInfo
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Gergő Tisza <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits