purgeParserCache.php: Implement --tag for purging one server only

Large wiki farms may have to purge servers concurrently (instead of
one at a time) in order to keep up with new writes and delete expired
rows faster than new rows are written.

The parameter for this uses server tags for three reasons:

* Maintenance risk and complexity.
  This requires the least information about MW configuration to be
  hardcoded in the scheduled maintenance cronjob (compared to: server
  indexes which are a runtime concept, or specific hostnames/IP/
  tableprefixes which may change and should not require
  coordinating changes elsewhere).

* Operational convenience.
  By using server tags, the parameters don't have to vary between
  data centers.

* Code complexity.
  The current code for obtaining connections is based on server indexes,
  which are easy to mapped at runtime from server tags. Other ways
  of identifying shard like hostnames are either an awkward fit (as
  they don't uniquely identify a shard per-se, with multiple instances
  on the same hardware at WMF), or require SqlBagOStuff to store and
  maintain more information about connections than it currently has
  readily available.

Bug: T282761
Change-Id: I618bc1e8ca3008a4dd54778ac24aa5948f27c52e
This commit is contained in:
Timo Tijhof 2021-06-24 17:40:05 +01:00
parent b30cb61b91
commit 0545fdc3af
7 changed files with 54 additions and 21 deletions

View file

@ -376,12 +376,15 @@ abstract class BagOStuff implements
* regularly during long-running operations with the percentage progress
* as the first parameter. [optional]
* @param int $limit Maximum number of keys to delete [default: INF]
* @param string|null $tag Tag to purge a single shard only.
* This is only supported when server tags are used in configuration.
* @return bool Success; false if unimplemented
*/
abstract public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
);
/**

View file

@ -149,9 +149,10 @@ class CachedBagOStuff extends BagOStuff {
public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
) {
$this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
$this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag );
return $this->store->proxyCall( __FUNCTION__, self::ARG0_NONKEY, self::RES_NONKEY, func_get_args() );
}

View file

@ -537,20 +537,11 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
return true;
}
/**
* Delete all objects expiring before a certain date.
* @param string|int $timestamp The reference date in MW or TS_UNIX format
* @param callable|null $progress Optional, a function which will be called
* regularly during long-running operations with the percentage progress
* as the first parameter. [optional]
* @param int $limit Maximum number of keys to delete [default: INF]
*
* @return bool Success; false if unimplemented
*/
public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
) {
return false;
}

View file

@ -255,11 +255,12 @@ class MultiWriteBagOStuff extends BagOStuff {
public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
) {
$ret = false;
foreach ( $this->caches as $cache ) {
if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag ) ) {
$ret = true;
}
}

View file

@ -167,7 +167,8 @@ class ReplicatedBagOStuff extends BagOStuff {
public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
) {
return $this->writeStore->proxyCall(
__FUNCTION__,

View file

@ -693,13 +693,19 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
$limit = INF,
string $tag = null
) {
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
$shardIndexes = $this->getServerShardIndexes();
shuffle( $shardIndexes );
if ( $tag !== null ) {
// Purge one server only, to support concurrent purging in large wiki farms (T282761).
$shardIndexes = [ $this->getServerIndexByTag( $tag ) ];
} else {
$shardIndexes = $this->getServerShardIndexes();
shuffle( $shardIndexes );
}
$ok = true;
@ -1173,6 +1179,23 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
return $shardIndexes;
}
/**
* @param string $tag
* @return int Server index for use with ::getConnection()
* @throws InvalidArgumentException If tag is unknown
*/
private function getServerIndexByTag( string $tag ) {
if ( !$this->serverTags ) {
throw new InvalidArgumentException( "Given a tag but no tags are configured" );
}
foreach ( $this->serverTags as $serverShardIndex => $serverTag ) {
if ( $tag === $serverTag ) {
return $serverShardIndex;
}
}
throw new InvalidArgumentException( "Unknown server tag: $tag" );
}
/**
* Wait for replica DBs to catch up to the master DB
*

View file

@ -58,6 +58,13 @@ class PurgeParserCache extends Maintenance {
$this->addOption( 'msleep', 'Milliseconds to sleep between purge chunks of $wgUpdateRowsPerQuery.',
false,
true );
$this->addOption(
'tag',
'Purge a single server only. This feature is designed for use by large wiki farms where ' .
'one has to purge multiple servers concurrently in order to keep up with new writes. ' .
'This requires using the SqlBagOStuff "servers" option in $wgObjectCaches.',
false,
true );
}
public function execute() {
@ -85,7 +92,13 @@ class PurgeParserCache extends Maintenance {
$this->output( "Deleting objects expiring before " . $humanDate . "\n" );
$pc = MediaWikiServices::getInstance()->getParserCache()->getCacheStorage();
$success = $pc->deleteObjectsExpiringBefore( $timestamp, [ $this, 'showProgressAndWait' ] );
$success = $pc->deleteObjectsExpiringBefore(
$timestamp,
[ $this, 'showProgressAndWait' ],
INF,
// Note that "0" can be a valid server tag, and must not be discarded or changed to null.
$this->getOption( 'tag', null )
);
if ( !$success ) {
$this->fatalError( "\nCannot purge this kind of parser cache." );
}