mediawiki-extensions-Echo/includes/SeenTime.php
Roan Kattouw ff165c0003 Make EchoSeenTime cache entries expire after 1 year
These entries weren't TTLed at all, and so we had millions of records
for inactive users that were being stored forever. If a user doesn't
view their notifications for a year, it's OK for their notifications
badge to go back to the unseen state until they click it.

(The fallback behavior on a cache miss is to act as if the seentime is
the UNIX epoch, which means that any unread notification they have is
more recent than it, and is considered unseen.)

Bug: T222851
Change-Id: I99230d2351b40751a3f2f5123c5f38693120259e
2019-10-18 12:05:52 +00:00

166 lines
4.1 KiB
PHP

<?php
use MediaWiki\MediaWikiServices;
/**
* A small wrapper around ObjectCache to manage
* storing the last time a user has seen notifications
*/
class EchoSeenTime {
/**
* Allowed notification types
* @var string[]
*/
private static $allowedTypes = [ 'alert', 'message' ];
/**
* @var User
*/
private $user;
/**
* @param User $user A logged in user
*/
private function __construct( User $user ) {
$this->user = $user;
}
/**
* @param User $user
* @return EchoSeenTime
*/
public static function newFromUser( User $user ) {
return new self( $user );
}
/**
* Hold onto a cache for our operations. Static so it can reuse the same
* in-process cache in different instances.
*
* @return BagOStuff
*/
private static function cache() {
static $wrappedCache = null;
// Use a configurable cache backend (T222851) and wrap it with CachedBagOStuff
// for an in-process cache (T144534)
if ( $wrappedCache === null ) {
$cacheConfig = MediaWikiServices::getInstance()->getMainConfig()->get( 'EchoSeenTimeCacheType' );
if ( $cacheConfig === null ) {
// EchoHooks::initEchoExtension sets EchoSeenTimeCacheType to $wgMainStash if it's
// null, so this can only happen if $wgMainStash is also null
throw new UnexpectedValueException(
'Either $wgEchoSeenTimeCacheType or $wgMainStash must be set'
);
}
return new CachedBagOStuff( ObjectCache::getInstance( $cacheConfig ) );
}
return $wrappedCache;
}
/**
* @param string $type Type of seen time to get
* @param int $format Format to return time in, defaults to TS_MW
* @return string|false Timestamp in specified format, or false if no stored time
*/
public function getTime( $type = 'all', $format = TS_MW ) {
$vals = [];
if ( $type === 'all' ) {
foreach ( self::$allowedTypes as $allowed ) {
// Use TS_MW, then convert later, so max works properly for
// all formats.
$vals[] = $this->getTime( $allowed, TS_MW );
}
return wfTimestamp( $format, min( $vals ) );
}
if ( !$this->validateType( $type ) ) {
return false;
}
$data = self::cache()->get( $this->getMemcKey( $type ) );
if ( $data === false ) {
// Check if the user still has it set in their preferences
$data = $this->user->getOption( 'echo-seen-time', false );
}
if ( $data === false ) {
// There is still no time set, so set time to the UNIX epoch.
// We can't remember their real seen time, so reset everything to
// unseen.
$data = wfTimestamp( TS_MW, 1 );
$this->setTime( $data, $type );
}
return wfTimestamp( $format, $data );
}
/**
* Sets the seen time
*
* @param string $time Time, in TS_MW format
* @param string $type Type of seen time to set
*/
public function setTime( $time, $type = 'all' ) {
if ( $type === 'all' ) {
foreach ( self::$allowedTypes as $allowed ) {
$this->setTime( $time, $allowed );
}
return;
}
if ( !$this->validateType( $type ) ) {
return;
}
// Write to the in-memory cache immediately, and defer writing to
// the real cache
$key = $this->getMemcKey( $type );
$cache = self::cache();
$cache->set( $key, $time, $cache::TTL_YEAR, BagOStuff::WRITE_CACHE_ONLY );
DeferredUpdates::addCallableUpdate( function () use ( $key, $time, $cache ) {
$cache->set( $key, $time, $cache::TTL_YEAR );
} );
}
/**
* Validate the given type, make sure it is allowed.
*
* @param string $type Given type
* @return bool Type is allowed
*/
private function validateType( $type ) {
return in_array( $type, self::$allowedTypes );
}
/**
* Build a memcached key.
*
* @param string $type Given notification type
* @return string Memcached key
*/
protected function getMemcKey( $type = 'all' ) {
$localKey = self::cache()->makeKey(
'echo', 'seen', $type, 'time', $this->user->getId()
);
if ( !$this->user->getOption( 'echo-cross-wiki-notifications' ) ) {
return $localKey;
}
$lookup = CentralIdLookup::factory();
$globalId = $lookup->centralIdFromLocalUser( $this->user, CentralIdLookup::AUDIENCE_RAW );
if ( !$globalId ) {
return $localKey;
}
return self::cache()->makeGlobalKey(
'echo', 'seen', $type, 'time', $globalId
);
}
}