Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace RedisCache with WANObjectCache #15

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 60 additions & 13 deletions .github/workflows/php-pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,40 @@ jobs:
# as this will disable xdebug which slows all processes significantly
coverage: none
extensions: ast
- uses: actions/checkout@v3

- name: Checkout Mediawiki
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: wikimedia/mediawiki
path: mediawiki
ref: ${{ matrix.mw }}

- name: Checkout RedisCache (dependency) extension
uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
repository: Wikia/RedisCache
path: mediawiki/extensions/RedisCache
path: extensions/Cheevos

- name: Checkout Twiggy (dependency) extension
uses: actions/checkout@v4
with:
repository: Wikia/mediawiki-extensions-Twiggy
path: extensions/Twiggy

- name: Checkout MobileFrontend (dependency) extension
uses: actions/checkout@v4
with:
repository: wikimedia/mediawiki-extensions-MobileFrontend
path: extensions/MobileFrontend
ref: ${{ matrix.mw }}

- name: Checkout HydraCore (dependency) extension
uses: actions/checkout@v4
with:
repository: Wikia/mediawiki-extensions-HydraCore
path: extensions/HydraCore

- name: Checkout Cheevos extension
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: mediawiki/extensions/Cheevos
path: extensions/Cheevos

- name: Cache Composer packages
id: composer-cache
Expand All @@ -52,16 +67,48 @@ jobs:

- name: Install composer dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
working-directory: ./mediawiki/extensions/Cheevos
working-directory: ./extensions/Cheevos
run: composer install --prefer-dist --no-progress

- name: Run PHPCS
working-directory: ./mediawiki/extensions/Cheevos
working-directory: ./extensions/Cheevos
run: composer phpcs

- name: Run Phan static analysis
working-directory: ./mediawiki/extensions/Cheevos
working-directory: ./extensions/Cheevos
run: composer phan
# Allow Phan to fail due to a high number of carryovers from the MW 1.39 upgrade
continue-on-error: true

- name: Run Phan static analysis
- name: Run jsonlint
working-directory: ./extensions/Cheevos
run: composer jsonlint

- name: Start MySQL
run: sudo systemctl start mysql.service

- name: Install MediaWiki composer dependencies
run: composer update --prefer-dist --no-progress

- name: Install & configure MediaWiki
run: |
php maintenance/install.php --dbtype mysql --dbuser root --dbpass root --pass TestPassword testwiki TestAdmin

echo 'error_reporting( E_ALL | E_STRICT );' >> LocalSettings.php
echo 'ini_set( "display_errors", 1 );' >> LocalSettings.php
echo '$wgShowExceptionDetails = true;' >> LocalSettings.php
echo '$wgShowDBErrorBacktrace = true;' >> LocalSettings.php
echo '$wgDevelopmentWarnings = true;' >> LocalSettings.php

echo '$wgNamespacesForEditPoints = [ NS_MAIN ];' >> LocalSettings.php

echo 'wfLoadExtension( "'Twiggy'" );' >> LocalSettings.php
echo 'wfLoadExtension( "'HydraCore'" );' >> LocalSettings.php
echo 'wfLoadExtension( "'MobileFrontend'" );' >> LocalSettings.php
echo 'wfLoadExtension( "'Cheevos'" );' >> LocalSettings.php

- name: Run schema changes
run: php maintenance/update.php --quick

- name: Run PHPUnit tests
run: php tests/phpunit/phpunit.php extensions/Cheevos/tests
15 changes: 12 additions & 3 deletions ServiceWiring.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
use Cheevos\CheevosHelper;
use Cheevos\FriendService;
use Fandom\Includes\Article\GlobalTitleLookup;
use GuzzleHttp\Client;
use MediaWiki\MediaWikiServices;
use Reverb\Notification\NotificationBroadcastFactory;
use WikiDomain\WikiConfigDataService;

return [
CheevosClient::class => static function ( MediaWikiServices $services ): CheevosClient {
$config = $services->getMainConfig();

// Use the shared HTTP client instance in the Fandom setup if available,
// but don't fail if it is absent (e.g. in tests).
$httpClient = defined( 'SERVICE_HTTP_CLIENT' ) ?
$services->getService( SERVICE_HTTP_CLIENT ) : new Client();
return new CheevosClient(
$services->getService( SERVICE_HTTP_CLIENT ),
$httpClient,
$config->get( 'CheevosHost' ),
[
'Accept' => 'application/json',
Expand All @@ -34,11 +40,14 @@
},

AchievementService::class => static function ( MediaWikiServices $services ): AchievementService {
// Make Reverb notifications an optional dependency to facilitate testing.
$notificationBroadcastFactory = $services->has( NotificationBroadcastFactory::class ) ?
$services->getService( NotificationBroadcastFactory::class ) : null;
return new AchievementService(
$services->getService( CheevosClient::class ),
$services->getService( RedisCache::class ),
$services->getMainWANObjectCache(),
$services->getMainConfig(),
$services->getService( NotificationBroadcastFactory::class ),
$notificationBroadcastFactory,
$services->getUserFactory(),
$services->getUserIdentityLookup()
);
Expand Down
186 changes: 50 additions & 136 deletions src/AchievementService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use RedisCache;
use RedisException;
use Reverb\Notification\NotificationBroadcastFactory;
use SpecialPage;
use WANObjectCache;

class AchievementService {
private const REDIS_CONNECTION_GROUP = 'cache';
private const CACHE_VERSION = 'v1';
private const TTL_5_MIN = 300;

public function __construct(
private CheevosClient $cheevosClient,
private RedisCache $redisCache,
private WANObjectCache $cache,
private Config $config,
private NotificationBroadcastFactory $notificationBroadcastFactory,
private ?NotificationBroadcastFactory $notificationBroadcastFactory,
private UserFactory $userFactory,
private UserIdentityLookup $userIdentityLookup
) {
Expand All @@ -39,7 +35,7 @@ public function broadcastAchievement( CheevosAchievement $achievement, string $s

$html = TemplateAchievements::achievementBlockPopUp( $achievement, $siteKey );

$broadcast = $this->notificationBroadcastFactory->newSystemSingle(
$broadcast = $this->notificationBroadcastFactory?->newSystemSingle(
'user-interest-achievement-earned',
$this->userFactory->newFromUserIdentity( $userIdentity ),
[
Expand All @@ -55,24 +51,7 @@ public function broadcastAchievement( CheevosAchievement $achievement, string $s

/** Invalidate API Cache */
public function invalidateCache(): void {
$redis = $this->redisCache->getConnection( self::REDIS_CONNECTION_GROUP );
if ( !$redis ) {
return;
}

$redisServers = $this->config->has( 'RedisServers' ) ? $this->config->get( 'RedisServers' ) : [];
$prefix = $redisServers['cache']['options']['prefix'] ?? '';

try {
$keys = $redis->getKeys( 'cheevos:apicache:*' );
foreach ( $keys as $key ) {
// remove prefix if exists, because weird.
$key = str_replace( $prefix . 'cheevos', 'cheevos', $key );
$redis->del( $key );
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}
$this->cache->touchCheckKey( $this->makeCheckKey() );
}

/**
Expand All @@ -81,77 +60,35 @@ public function invalidateCache(): void {
* @return CheevosAchievement[]
*/
public function getAchievements( ?string $siteKey = null ): array {
$redis = $this->redisCache->getConnection( self::REDIS_CONNECTION_GROUP );
if ( !$redis ) {
return $this->cheevosClient->parse(
$this->cheevosClient->get( 'achievements/all', [ 'site_key' => $siteKey, 'limit' => 0 ] ),
'achievements',
CheevosAchievement::class
);
}

$redisKey = $this->makeRedisKey( 'getAchievements', self::CACHE_VERSION, $siteKey ?: 'all' );
try {
$cachedValue = json_decode( $redis->get( $redisKey ), true );
if ( !empty( $cachedValue ) ) {
return $this->cache->getWithSetCallback(
$this->cache->makeGlobalKey( 'cheevos', 'achievements', $siteKey ?? 'all' ),
5 * $this->cache::TTL_MINUTE,
function () use ( $siteKey ) {
return $this->cheevosClient->parse(
$cachedValue,
$this->cheevosClient->get( 'achievements/all', [ 'site_key' => $siteKey, 'limit' => 0 ] ),
'achievements',
CheevosAchievement::class
);
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}

$response = $this->cheevosClient->get( 'achievements/all', [ 'site_key' => $siteKey, 'limit' => 0 ] );
try {
if ( isset( $response['achievements'] ) ) {
$redis->setEx( $redisKey, self::TTL_5_MIN, json_encode( $response ) );
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}

return $this->cheevosClient->parse( $response, 'achievements', CheevosAchievement::class );
},
[ 'checkKeys' => [ $this->makeCheckKey() ] ]
);
}

/** Get achievement by database ID with caching. */
public function getAchievement( int $id ): ?CheevosAchievement {
$redis = $this->redisCache->getConnection( self::REDIS_CONNECTION_GROUP );
if ( !$redis ) {
$response = $this->cheevosClient->get( "achievement/$id" );
return $this->cheevosClient->parse(
[ $response ],
'achievements',
CheevosAchievement::class,
true
);
}

$redisKey = $this->makeRedisKey( 'getAchievement', self::CACHE_VERSION, $id );
try {
$cachedValue = json_decode( $redis->get( $redisKey ), true );
if ( !empty( $cachedValue ) ) {
return $this->cache->getWithSetCallback(
$this->cache->makeGlobalKey( 'cheevos', 'achievement', $id ),
5 * $this->cache::TTL_MINUTE,
function () use ( $id ) {
return $this->cheevosClient->parse(
[ $cachedValue ],
[ $this->cheevosClient->get( "achievement/$id" ) ],
'achievements',
CheevosAchievement::class,
true
);
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}

$response = $this->cheevosClient->get( "achievement/$id" );
try {
$redis->setEx( $redisKey, self::TTL_5_MIN, json_encode( $response ) );
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}

return $this->cheevosClient->parse( [ $response ], 'achievements', CheevosAchievement::class, true );
},
[ 'checkKeys' => [ $this->makeCheckKey() ] ]
);
}

/** Soft delete an achievement from the service. */
Expand Down Expand Up @@ -245,62 +182,34 @@ public function putProgress( array $body ): array {
* @return CheevosAchievementCategory[]
*/
public function getCategories( bool $skipCache = false ): array {
$redis = $this->redisCache->getConnection( self::REDIS_CONNECTION_GROUP );
$redisKey = $this->makeRedisKey( 'getCategories', self::CACHE_VERSION );

if ( !$skipCache && $redis ) {
try {
$cachedValue = json_decode( $redis->get( $redisKey ), true );
if ( !empty( $cachedValue ) ) {
return $this->cheevosClient->parse( $cachedValue, 'categories', CheevosAchievementCategory::class );
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}
}
$getCategoriesFromService = function () {
$response = $this->cheevosClient->get( 'achievement_categories/all', [ 'limit' => 0 ] );
return $this->cheevosClient->parse( $response, 'categories', CheevosAchievementCategory::class );
};

$response = $this->cheevosClient->get( 'achievement_categories/all', [ 'limit' => 0 ] );
if ( $redis ) {
try {
$redis->setEx( $redisKey, self::TTL_5_MIN, json_encode( $response ) );
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}
if ( $skipCache ) {
return $getCategoriesFromService();
}
return $this->cheevosClient->parse( $response, 'categories', CheevosAchievementCategory::class );

return $this->cache->getWithSetCallback(
$this->cache->makeGlobalKey( 'cheevos', 'categories' ),
5 * $this->cache::TTL_MINUTE,
$getCategoriesFromService,
[ 'checkKeys' => [ $this->makeCheckKey() ] ]
);
}

/** Get Category by ID */
public function getCategory( int $id ): ?CheevosAchievementCategory {
$redis = $this->redisCache->getConnection( self::REDIS_CONNECTION_GROUP );

if ( !$redis ) {
$response = $this->cheevosClient->get( "achievement_category/$id" );
return $this->cheevosClient->parse( $response, 'categories', CheevosAchievementCategory::class, true );
}

$redisKey = $this->makeRedisKey( 'getCategory', self::CACHE_VERSION, $id );
try {
$cachedValue = json_decode( $redis->get( $redisKey ), true );
if ( !empty( $cachedValue ) ) {
return $this->cheevosClient->parse(
$cachedValue,
'categories',
CheevosAchievementCategory::class,
true
);
}
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}

$response = $this->cheevosClient->get( "achievement_category/$id" );
try {
$redis->setEx( $redisKey, self::TTL_5_MIN, json_encode( $response ) );
} catch ( RedisException $e ) {
wfDebug( __METHOD__ . ": Caught RedisException - " . $e->getMessage() );
}
return $this->cheevosClient->parse( $response, 'categories', CheevosAchievementCategory::class, true );
return $this->cache->getWithSetCallback(
$this->cache->makeGlobalKey( 'cheevos', 'category', $id ),
5 * $this->cache::TTL_MINUTE,
function () use ( $id ) {
$response = $this->cheevosClient->get( "achievement_category/$id" );
return $this->cheevosClient->parse( $response, 'categories', CheevosAchievementCategory::class, true );
},
[ 'checkKeys' => [ $this->makeCheckKey() ] ]
);
}

/** Delete Category by ID (with optional user_id for user that deleted the category) */
Expand Down Expand Up @@ -471,7 +380,12 @@ private function parseFilters( array $filters, ?UserIdentity $userIdentity, ?int
return $filters;
}

private function makeRedisKey( ...$parts ): string {
return 'cheevos:apicache:' . implode( ':', $parts );
/**
* Make a global "check key" allowing for one-touch invalidation of all Cheevos cache keys.
* @return string
*/
private function makeCheckKey(): string {
return $this->cache->makeGlobalKey( 'cheevos', 'check' );
}

}
Loading
Loading