mediawiki-extensions-AbuseF.../includes/EmergencyCache.php

141 lines
3.6 KiB
PHP
Raw Normal View History

<?php
namespace MediaWiki\Extension\AbuseFilter;
use BagOStuff;
/**
* Helper class for EmergencyWatcher. Wrapper around cache which tracks hits of recently
* modified filters.
*/
class EmergencyCache {
public const SERVICE_NAME = 'AbuseFilterEmergencyCache';
/** @var BagOStuff */
private $stash;
/** @var int[] */
private $ttlPerGroup;
/**
* @param BagOStuff $stash
* @param int[] $ttlPerGroup
*/
public function __construct( BagOStuff $stash, array $ttlPerGroup ) {
$this->stash = $stash;
$this->ttlPerGroup = $ttlPerGroup;
}
/**
* Get recently modified filters in the group. Thanks to this, performance can be improved,
* because only a small subset of filters will need an update.
*
* @param string $group
* @return int[]
*/
public function getFiltersToCheckInGroup( string $group ) : array {
$filterToExpiry = $this->stash->get( $this->createGroupKey( $group ) );
if ( $filterToExpiry === false ) {
return [];
}
$time = (int)round( $this->stash->getCurrentTime() );
return array_keys( array_filter(
$filterToExpiry,
static function ( $exp ) use ( $time ) {
return $exp > $time;
}
) );
}
/**
* Create a new entry in cache for a filter and update the entry for the group.
* This method is usually called after the filter has been updated.
*
* @param int $filter
* @param string $group
* @return bool
*/
public function setNewForFilter( int $filter, string $group ) : bool {
$ttl = $this->ttlPerGroup[$group] ?? $this->ttlPerGroup['default'];
$expiry = (int)round( $this->stash->getCurrentTime() + $ttl );
$this->stash->merge(
$this->createGroupKey( $group ),
function ( $cache, $key, $value ) use ( $filter, $expiry ) {
if ( $value === false ) {
$value = [];
}
// note that some filters may have already had their keys expired
// we are currently filtering them out in getFiltersToCheckInGroup
// but if necessary, it can be done here
$value[$filter] = $expiry;
return $value;
},
$expiry
);
return $this->stash->set(
$this->createFilterKey( $filter ),
[ 'total' => 0, 'matches' => 0, 'expiry' => $expiry ],
$expiry
);
}
/**
* Increase the filter's 'total' value by one and possibly also the 'matched' value.
*
* @param int $filter
* @param bool $matched Whether the filter matched the action
* @return bool
*/
public function incrementForFilter( int $filter, bool $matched ) : bool {
return $this->stash->merge(
$this->createFilterKey( $filter ),
static function ( $cache, $key, $value, &$expiry ) use ( $matched ) {
if ( $value === false ) {
return false;
}
$value['total']++;
if ( $matched ) {
$value['matches']++;
}
// enforce the prior TTL
$expiry = $value['expiry'];
return $value;
}
);
}
/**
* Get the cache entry for the filter. Returns false when the key has already expired.
* Otherwise it returns the entry formatted as [ 'total' => number of actions,
* 'matches' => number of hits ] (since the last filter modification).
*
* @param int $filter
* @return array|false
*/
public function getForFilter( int $filter ) {
$value = $this->stash->get( $this->createFilterKey( $filter ) );
if ( $value !== false ) {
unset( $value['expiry'] );
}
return $value;
}
/**
* @param string $group
* @return string
*/
private function createGroupKey( string $group ) : string {
return $this->stash->makeKey( 'abusefilter', 'emergency', 'group', $group );
}
/**
* @param int $filter
* @return string
*/
private function createFilterKey( int $filter ) : string {
return $this->stash->makeKey( 'abusefilter', 'emergency', 'filter', $filter );
}
}