* Updated wikimedia/at-ease from 1.2.0 to 2.0.0.
* Updated wikimedia/remex-html from 2.0.1 to 2.0.3.
* Updated monolog/monolog from 1.22.1 to 1.24.0 (dev-only).
-* Updated wikimedia/object-factory from 1.0.0 to 2.0.0.
+* Updated wikimedia/object-factory from 1.0.0 to 2.1.0.
* Updated wikimedia/timestamp from 2.2.0 to 3.0.0.
* Updated wikimedia/xmp-reader from 0.6.2 to 0.6.3.
* Updated mediawiki/mediawiki-phan-config from 0.6.0 to 0.6.1 (dev-only).
* Previously, when iterating ResultWrapper with foreach() or a similar
construct, the range of the index was 1..numRows. This has been fixed to be
0..(numRows-1).
+* The ChangePasswordForm hook, deprecated in 1.27, has been removed. Use the
+ AuthChangeFormFields hook or security levels instead.
+* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed.
+ Use WikiMap::getWikiIdFromDbDomain() instead.
* …
=== Deprecations in 1.34 ===
"wikimedia/html-formatter": "1.0.2",
"wikimedia/ip-set": "2.0.1",
"wikimedia/less.php": "1.8.0",
- "wikimedia/object-factory": "2.0.0",
+ "wikimedia/object-factory": "2.1.0",
"wikimedia/password-blacklist": "0.1.4",
"wikimedia/php-session-serializer": "1.0.7",
"wikimedia/purtle": "1.0.7",
$req: AuthenticationRequest object describing the change (and target user)
$status: StatusValue with the result of the action
-'ChangePasswordForm': DEPRECATED since 1.27! Use AuthChangeFormFields or
-security levels. For extensions that need to add a field to the ChangePassword
-form via the Preferences form.
-&$extraFields: An array of arrays that hold fields like would be passed to the
- pretty function.
-
'ChangesListInitRows': Batch process change list rows prior to rendering.
$changesList: ChangesList instance
$rows: The data that will be rendered. May be a \Wikimedia\Rdbms\IResultWrapper
*/
$wgPageInfoTransclusionLimit = 50;
-/**
- * Set this to an integer to only do synchronous site_stats updates
- * one every *this many* updates. The other requests go into pending
- * delta values in $wgMemc. Make sure that $wgMemc is a global cache.
- * If set to -1, updates *only* go to $wgMemc (useful for daemons).
- */
-$wgSiteStatsAsyncFactor = false;
-
/**
* Parser test suite files to be run by parserTests.php when no specific
* filename is passed to it.
}
/**
- * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit
+ * Raise PHP's memory limit (if needed).
*
- * @return int Resulting value of the memory limit.
+ * @internal For use by Setup.php
*/
-function wfMemoryLimit() {
- global $wgMemoryLimit;
- $memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
- if ( $memlimit != -1 ) {
- $conflimit = wfShorthandToInteger( $wgMemoryLimit );
- if ( $conflimit == -1 ) {
+function wfMemoryLimit( $newLimit ) {
+ $oldLimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
+ // If the INI config is already unlimited, there is nothing larger
+ if ( $oldLimit != -1 ) {
+ $newLimit = wfShorthandToInteger( $newLimit );
+ if ( $newLimit == -1 ) {
wfDebug( "Removing PHP's memory limit\n" );
Wikimedia\suppressWarnings();
- ini_set( 'memory_limit', $conflimit );
+ ini_set( 'memory_limit', $newLimit );
Wikimedia\restoreWarnings();
- return $conflimit;
- } elseif ( $conflimit > $memlimit ) {
- wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" );
+ } elseif ( $newLimit > $oldLimit ) {
+ wfDebug( "Raising PHP's memory limit to $newLimit bytes\n" );
Wikimedia\suppressWarnings();
- ini_set( 'memory_limit', $conflimit );
+ ini_set( 'memory_limit', $newLimit );
Wikimedia\restoreWarnings();
- return $conflimit;
}
}
- return $memlimit;
}
/**
*/
public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
// TODO: pass in a DBTransactionContext instead of a database connection.
- $this->checkDatabaseWikiId( $dbw );
+ $this->checkDatabaseDomain( $dbw );
$slotRoles = $rev->getSlotRoles();
$minor,
User $user
) {
- $this->checkDatabaseWikiId( $dbw );
+ $this->checkDatabaseDomain( $dbw );
$pageId = $title->getArticleID();
* @param IDatabase $db
* @throws MWException
*/
- private function checkDatabaseWikiId( IDatabase $db ) {
- $storeWiki = $this->dbDomain;
- $dbWiki = $db->getDomainID();
-
- if ( $dbWiki === $storeWiki ) {
- return;
- }
-
- $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
- // @FIXME: when would getDomainID() be false here?
- $dbWiki = $dbWiki ?: wfWikiID();
-
- if ( $dbWiki === $storeWiki ) {
- return;
- }
-
- // HACK: counteract encoding imposed by DatabaseDomain
- $storeWiki = str_replace( '?h', '-', $storeWiki );
- $dbWiki = str_replace( '?h', '-', $dbWiki );
-
- if ( $dbWiki === $storeWiki ) {
+ private function checkDatabaseDomain( IDatabase $db ) {
+ $dbDomain = $db->getDomainID();
+ $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
+ if ( $dbDomain === $storeDomain ) {
return;
}
- throw new MWException( "RevisionStore for $storeWiki "
- . "cannot be used with a DB connection for $dbWiki" );
+ throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
}
/**
* @return object|false data row as a raw object
*/
private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
- $this->checkDatabaseWikiId( $db );
+ $this->checkDatabaseDomain( $db );
$revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
$options = [];
* of the corresponding revision.
*/
public function listRevisionSizes( IDatabase $db, array $revIds ) {
- $this->checkDatabaseWikiId( $db );
+ $this->checkDatabaseDomain( $db );
$revLens = [];
if ( !$revIds ) {
* @return int
*/
private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
- $this->checkDatabaseWikiId( $db );
+ $this->checkDatabaseDomain( $db );
if ( $rev->getPageId() === null ) {
return 0;
* @return int
*/
public function countRevisionsByPageId( IDatabase $db, $id ) {
- $this->checkDatabaseWikiId( $db );
+ $this->checkDatabaseDomain( $db );
$row = $db->selectRow( 'revision',
[ 'revCount' => 'COUNT(*)' ],
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
- $this->checkDatabaseWikiId( $db );
+ $this->checkDatabaseDomain( $db );
if ( !$userId ) {
return false;
// Start the autoloader, so that extensions can derive classes from core files
require_once "$IP/includes/AutoLoader.php";
-// Load up some global defines
+// Load global constants
require_once "$IP/includes/Defines.php";
// Load default settings
die( 1 );
}
+/**
+ * Changes to the PHP environment that don't vary on configuration.
+ */
+
// Install a header callback
MediaWiki\HeaderCallback::register();
+// Set the encoding used by reading HTTP input, writing HTTP output.
+// This is also the default for mbstring functions.
+mb_internal_encoding( 'UTF-8' );
+
/**
* Load LocalSettings.php
*/
// Don't let any other extensions load
ExtensionRegistry::getInstance()->finish();
-mb_internal_encoding( 'UTF-8' );
-
// Set the configured locale on all requests for consisteny
putenv( "LC_ALL=$wgShellLocale" );
setlocale( LC_ALL, $wgShellLocale );
$ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc' );
// Raise the memory limit if it's too low
-wfMemoryLimit();
+// Note, this makes use of wfDebug, and thus should not be before
+// MWDebug::init() is called.
+wfMemoryLimit( $wgMemoryLimit );
/**
* Set up the timezone, suppressing the pseudo-security warning in PHP 5.1+
$config = MediaWikiServices::getInstance()->getMainConfig();
$lb = self::getLB();
- $dbr = $lb->getConnection( DB_REPLICA );
+ $dbr = $lb->getConnectionRef( DB_REPLICA );
wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
$row = self::doLoadFromDB( $dbr );
if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
// Might have just been initialized during this request? Underflow?
wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
- $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+ $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
}
if ( !self::isRowSane( $row ) ) {
SiteStatsInit::doAllAndCommit( $dbr );
}
- $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+ $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) );
}
if ( !self::isRowSane( $row ) ) {
$cache->makeKey( 'SiteStats', 'groupcounts', $group ),
$cache::TTL_HOUR,
function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) {
- $dbr = self::getLB()->getConnection( DB_REPLICA );
+ $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
return (int)$dbr->selectField(
$cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
$cache::TTL_HOUR,
function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns, $fname ) {
- $dbr = self::getLB()->getConnection( DB_REPLICA );
+ $dbr = self::getLB()->getConnectionRef( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
return (int)$dbr->selectField(
}
}
- /**
- * @return bool|string
- */
- private function getWikiId() {
- // TODO: get from RevisionStore
- return false;
- }
-
/**
* Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting
* the given revision.
// TODO: In the wiring, register a listener for this on the new PageEventEmitter
ResourceLoaderWikiModule::invalidateModuleCache(
- $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?: wfWikiID()
+ $title,
+ $oldLegacyRevision,
+ $legacyRevision,
+ $this->loadbalancerFactory->getLocalDomainID()
);
$this->doTransition( 'done' );
// the stash request finishes parsing. For the lock acquisition below, there is not much
// need to duplicate parsing of the same content/user/summary bundle, so try to avoid
// blocking at all here.
- $dbw = $this->lb->getConnection( DB_MASTER );
+ $dbw = $this->lb->getConnectionRef( DB_MASTER );
if ( !$dbw->lock( $key, $fname, 0 ) ) {
// De-duplicate requests on the same key
return self::ERROR_BUSY;
* @return string|null TS_MW timestamp or null
*/
private function lastEditTime( User $user ) {
- $db = $this->lb->getConnection( DB_REPLICA );
+ $db = $this->lb->getConnectionRef( DB_REPLICA );
+
$actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
$time = $db->selectField(
[ 'recentchanges' ] + $actorQuery['tables'],
* @since 1.20
*/
public function getSubpage( $text ) {
- return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text );
+ return self::makeTitleSafe(
+ $this->mNamespace,
+ $this->getText() . '/' . $text,
+ '',
+ $this->mInterwiki
+ );
}
/**
* Get the timestamp when this page was updated since the user last saw it.
*
* @param User|null $user
- * @return string|null
+ * @return string|bool|null String timestamp, false if not watched, null if nothing is unseen
*/
public function getNotificationTimestamp( $user = null ) {
global $wgUser;
: (string)$domain->getDatabase();
}
- /**
- * @param string $domain
- * @return string
- * @deprecated Since 1.33; use getWikiIdFromDbDomain()
- */
- public static function getWikiIdFromDomain( $domain ) {
- return self::getWikiIdFromDbDomain( $domain );
- }
-
/**
* @return DatabaseDomain Database domain of the current wiki
* @since 1.33
* @since 1.33
*/
public static function isCurrentWikiDbDomain( $domain ) {
- return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) );
+ return self::getCurrentWikiDbDomain()->equals( $domain );
}
/**
$out = $this->getOutput();
$request = $this->getRequest();
- /**
- * Allow client caching.
- */
- if ( $out->checkLastModified( $this->page->getTouched() ) ) {
+ // Allow client-side HTTP caching of the history page.
+ // But, always ignore this cache if the (logged-in) user has this page on their watchlist
+ // and has one or more unseen revisions. Otherwise, we might be showing stale update markers.
+ // The Last-Modified for the history page does not change when user's markers are cleared,
+ // so going from "some unseen" to "all seen" would not clear the cache.
+ // But, when all of the revisions are marked as seen, then only way for new unseen revision
+ // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified.
+ if (
+ !$this->hasUnseenRevisionMarkers() &&
+ $out->checkLastModified( $this->page->getTouched() )
+ ) {
return null; // Client cache fresh and headers sent, nothing more to do.
}
return null;
}
+ /**
+ * @return bool Page is watched by and has unseen revision for the user
+ */
+ private function hasUnseenRevisionMarkers() {
+ return (
+ $this->getContext()->getConfig()->get( 'ShowUpdatedMarker' ) &&
+ $this->getTitle()->getNotificationTimestamp( $this->getUser() )
+ );
+ }
+
/**
* Fetch an array of revisions, specified by a given limit, offset and
* direction. This is now only used by the feeds. It was previously
return [];
}
- $db = $db ?: $this->loadBalancer->getConnection( DB_REPLICA );
+ $db = $db ?: $this->loadBalancer->getConnectionRef( DB_REPLICA );
$result = $db->select(
[ 'ipblocks_restrictions', 'page' ],
return false;
}
- $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
$dbw->insert(
'ipblocks_restrictions',
* @return bool
*/
public function update( array $restrictions ) {
- $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$parentBlockId = (int)$parentBlockId;
- $db = $this->loadBalancer->getConnection( DB_MASTER );
+ $db = $this->loadBalancer->getConnectionRef( DB_MASTER );
$db->startAtomic( __METHOD__ );
* @return bool
*/
public function delete( array $restrictions ) {
- $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
$result = true;
foreach ( $restrictions as $restriction ) {
if ( !$restriction instanceof Restriction ) {
* @return bool
*/
public function deleteByBlockId( $blockId ) {
- $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
return $dbw->delete(
'ipblocks_restrictions',
[ 'ir_ipb_id' => $blockId ],
* @return bool
*/
public function deleteByParentBlockId( $parentBlockId ) {
- $dbw = $this->loadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
return $dbw->deleteJoin(
'ipblocks_restrictions',
'ipblocks',
* Class for handling updates to the site_stats table
*/
class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
- /** @var BagOStuff */
- protected $stash;
/** @var int */
protected $edits = 0;
/** @var int */
/** @var int */
protected $images = 0;
- private static $counters = [ 'edits', 'pages', 'articles', 'users', 'images' ];
+ /** @var string[] Map of (table column => counter type) */
+ private static $counters = [
+ 'ss_total_edits' => 'edits',
+ 'ss_total_pages' => 'pages',
+ 'ss_good_articles' => 'articles',
+ 'ss_users' => 'users',
+ 'ss_images' => 'images'
+ ];
// @todo deprecate this constructor
function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) {
$this->articles = $good;
$this->pages = $pages;
$this->users = $users;
-
- $this->stash = MediaWikiServices::getInstance()->getMainObjectStash();
}
public function merge( MergeableUpdate $update ) {
}
/**
- * @param array $deltas
+ * @param int[] $deltas Map of (counter type => integer delta)
* @return SiteStatsUpdate
+ * @throws UnexpectedValueException
*/
public static function factory( array $deltas ) {
$update = new self( 0, 0, 0 );
}
foreach ( self::$counters as $field ) {
- if ( isset( $deltas[$field] ) && $deltas[$field] ) {
- $update->$field = $deltas[$field];
- }
+ $update->$field = $deltas[$field] ?? 0;
}
return $update;
}
public function doUpdate() {
- $this->doUpdateContextStats();
-
- $rate = MediaWikiServices::getInstance()->getMainConfig()->get( 'SiteStatsAsyncFactor' );
- // If set to do so, only do actual DB updates 1 every $rate times.
- // The other times, just update "pending delta" values in memcached.
- if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
- $this->doUpdatePendingDeltas();
- } else {
- // Need a separate transaction because this a global lock
- DeferredUpdates::addCallableUpdate( [ $this, 'tryDBUpdateInternal' ] );
- }
- }
-
- /**
- * Do not call this outside of SiteStatsUpdate
- */
- public function tryDBUpdateInternal() {
$services = MediaWikiServices::getInstance();
- $config = $services->getMainConfig();
-
- $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
- $lockKey = $dbw->getDomainID() . ':site_stats'; // prepend wiki ID
- $pd = [];
- if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
- // Lock the table so we don't have double DB/memcached updates
- if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
- $this->doUpdatePendingDeltas();
+ $stats = $services->getStatsdDataFactory();
- return;
+ $deltaByType = [];
+ foreach ( self::$counters as $type ) {
+ $delta = $this->$type;
+ if ( $delta !== 0 ) {
+ $stats->updateCount( "site.$type", $delta );
}
- $pd = $this->getPendingDeltas();
- // Piggy-back the async deltas onto those of this stats update....
- $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] );
- $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] );
- $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] );
- $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] );
- $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] );
- }
-
- // Build up an SQL query of deltas and apply them...
- $updates = '';
- $this->appendUpdate( $updates, 'ss_total_edits', $this->edits );
- $this->appendUpdate( $updates, 'ss_good_articles', $this->articles );
- $this->appendUpdate( $updates, 'ss_total_pages', $this->pages );
- $this->appendUpdate( $updates, 'ss_users', $this->users );
- $this->appendUpdate( $updates, 'ss_images', $this->images );
- if ( $updates != '' ) {
- $dbw->update( 'site_stats', [ $updates ], [], __METHOD__ );
+ $deltaByType[$type] = $delta;
}
- if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
- // Decrement the async deltas now that we applied them
- $this->removePendingDeltas( $pd );
- // Commit the updates and unlock the table
- $dbw->unlock( $lockKey, __METHOD__ );
- }
+ ( new AutoCommitUpdate(
+ $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER ),
+ __METHOD__,
+ function ( IDatabase $dbw, $fname ) use ( $deltaByType ) {
+ $set = [];
+ foreach ( self::$counters as $column => $type ) {
+ $delta = (int)$deltaByType[$type];
+ if ( $delta > 0 ) {
+ $set[] = "$column=$column+" . abs( $delta );
+ } elseif ( $delta < 0 ) {
+ $set[] = "$column=$column-" . abs( $delta );
+ }
+ }
+
+ if ( $set ) {
+ $dbw->update( 'site_stats', $set, [ 'ss_row_id' => 1 ], $fname );
+ }
+ }
+ ) )->doUpdate();
- // Invalid cache used by parser functions
+ // Invalidate cache used by parser functions
SiteStats::unload();
}
$services = MediaWikiServices::getInstance();
$config = $services->getMainConfig();
- $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
+ $dbr = $services->getDBLoadBalancer()->getConnectionRef( DB_REPLICA, 'vslow' );
# Get non-bot users than did some recent action other than making accounts.
# If account creation is included, the number gets inflated ~20+ fold on enwiki.
$rcQuery = RecentChange::getQueryInfo();
return $activeUsers;
}
-
- protected function doUpdateContextStats() {
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- foreach ( [ 'edits', 'articles', 'pages', 'users', 'images' ] as $type ) {
- $delta = $this->$type;
- if ( $delta !== 0 ) {
- $stats->updateCount( "site.$type", $delta );
- }
- }
- }
-
- protected function doUpdatePendingDeltas() {
- $this->adjustPending( 'ss_total_edits', $this->edits );
- $this->adjustPending( 'ss_good_articles', $this->articles );
- $this->adjustPending( 'ss_total_pages', $this->pages );
- $this->adjustPending( 'ss_users', $this->users );
- $this->adjustPending( 'ss_images', $this->images );
- }
-
- /**
- * @param string &$sql
- * @param string $field
- * @param int $delta
- */
- protected function appendUpdate( &$sql, $field, $delta ) {
- if ( $delta ) {
- if ( $sql ) {
- $sql .= ',';
- }
- if ( $delta < 0 ) {
- $sql .= "$field=$field-" . abs( $delta );
- } else {
- $sql .= "$field=$field+" . abs( $delta );
- }
- }
- }
-
- /**
- * @param BagOStuff $stash
- * @param string $type
- * @param string $sign ('+' or '-')
- * @return string
- */
- private function getTypeCacheKey( BagOStuff $stash, $type, $sign ) {
- return $stash->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
- }
-
- /**
- * Adjust the pending deltas for a stat type.
- * Each stat type has two pending counters, one for increments and decrements
- * @param string $type
- * @param int $delta Delta (positive or negative)
- */
- protected function adjustPending( $type, $delta ) {
- if ( $delta < 0 ) { // decrement
- $key = $this->getTypeCacheKey( $this->stash, $type, '-' );
- } else { // increment
- $key = $this->getTypeCacheKey( $this->stash, $type, '+' );
- }
-
- $magnitude = abs( $delta );
- $this->stash->incrWithInit( $key, 0, $magnitude, $magnitude );
- }
-
- /**
- * Get pending delta counters for each stat type
- * @return array Positive and negative deltas for each type
- */
- protected function getPendingDeltas() {
- $pending = [];
- foreach ( [ 'ss_total_edits',
- 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ] as $type
- ) {
- // Get pending increments and pending decrements
- $flg = BagOStuff::READ_LATEST;
- $pending[$type]['+'] = (int)$this->stash->get(
- $this->getTypeCacheKey( $this->stash, $type, '+' ),
- $flg
- );
- $pending[$type]['-'] = (int)$this->stash->get(
- $this->getTypeCacheKey( $this->stash, $type, '-' ),
- $flg
- );
- }
-
- return $pending;
- }
-
- /**
- * Reduce pending delta counters after updates have been applied
- * @param array $pd Result of getPendingDeltas(), used for DB update
- */
- protected function removePendingDeltas( array $pd ) {
- foreach ( $pd as $type => $deltas ) {
- foreach ( $deltas as $sign => $magnitude ) {
- // Lower the pending counter now that we applied these changes
- $key = $this->getTypeCacheKey( $this->stash, $type, $sign );
- $this->stash->decr( $key, $magnitude );
- }
- }
- }
}
*/
public function doUpdate() {
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- $dbw = $lb->getConnection( DB_MASTER );
+ $dbw = $lb->getConnectionRef( DB_MASTER );
$fname = __METHOD__;
( new AutoCommitUpdate( $dbw, __METHOD__, function () use ( $lb, $dbw, $fname ) {
// The user_editcount is probably NULL (e.g. not initialized).
// Since this update runs after the new revisions were committed,
// wait for the replica DB to catch up so they will be counted.
- $dbr = $lb->getConnection( DB_REPLICA );
- // If $dbr is actually the master DB, then clearing the snapshot is
+ $dbr = $lb->getConnectionRef( DB_REPLICA );
+ // If $dbr is actually the master DB, then clearing the snapshot
// is harmless and waitForMasterPos() will just no-op.
$dbr->flushSnapshot( $fname );
$lb->waitForMasterPos( $dbr );
/** @var StatsdDataFactoryInterface */
protected $stats;
- /** @var BagOStuff */
- protected $dupCache;
+ /** @var WANObjectCache */
+ protected $wanCache;
const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
/**
* @param array $params
+ * - type : A job type
+ * - domain : A DB domain ID
+ * - wanCache : An instance of WANObjectCache to use for caching [default: none]
+ * - stats : An instance of StatsdDataFactoryInterface [default: none]
+ * - claimTTL : Seconds a job can be claimed for exclusive execution [default: forever]
+ * - maxTries : Total times a job can be tried, assuming claims expire [default: 3]
+ * - order : Queue order, one of ("fifo", "timestamp", "random") [default: variable]
+ * - readOnlyReason : Mark the queue as read-only with this reason [default: false]
* @throws JobQueueError
*/
protected function __construct( array $params ) {
}
$this->readOnlyReason = $params['readOnlyReason'] ?? false;
$this->stats = $params['stats'] ?? new NullStatsdDataFactory();
- $this->dupCache = $params['stash'] ?? new EmptyBagOStuff();
+ $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
}
/**
* @return bool
*/
protected function doDeduplicateRootJob( IJobSpecification $job ) {
- if ( !$job->hasRootJobParams() ) {
+ $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+ if ( !$params ) {
throw new JobQueueError( "Cannot register root job; missing parameters." );
}
- $params = $job->getRootJobParams();
$key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
- // Callers should call JobQueueGroup::push() before this method so that if the insert
- // fails, the de-duplication registration will be aborted. Since the insert is
- // deferred till "transaction idle", do the same here, so that the ordering is
- // maintained. Having only the de-duplication registration succeed would cause
- // jobs to become no-ops without any actual jobs that made them redundant.
- $timestamp = $this->dupCache->get( $key ); // current last timestamp of this job
- if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+ // Callers should call JobQueueGroup::push() before this method so that if the
+ // insert fails, the de-duplication registration will be aborted. Having only the
+ // de-duplication registration succeed would cause jobs to become no-ops without
+ // any actual jobs that made them redundant.
+ $timestamp = $this->wanCache->get( $key ); // last known timestamp of such a root job
+ if ( $timestamp !== false && $timestamp >= $params['rootJobTimestamp'] ) {
return true; // a newer version of this root job was enqueued
}
// Update the timestamp of the last root job started at the location...
- return $this->dupCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+ return $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
}
/**
if ( $job->getType() !== $this->type ) {
throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
}
- $isDuplicate = $this->doIsRootJobOldDuplicate( $job );
- return $isDuplicate;
+ return $this->doIsRootJobOldDuplicate( $job );
}
/**
* @return bool
*/
protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
- if ( !$job->hasRootJobParams() ) {
+ $params = $job->hasRootJobParams() ? $job->getRootJobParams() : null;
+ if ( !$params ) {
return false; // job has no de-deplication info
}
- $params = $job->getRootJobParams();
$key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
// Get the last time this root job was enqueued
- $timestamp = $this->dupCache->get( $key );
+ $timestamp = $this->wanCache->get( $key );
+ if ( $timestamp === false || $params['rootJobTimestamp'] > $timestamp ) {
+ // Update the timestamp of the last known root job started at the location...
+ $this->wanCache->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL );
+ }
// Check if a new root job was started at the location after this one's...
return ( $timestamp && $timestamp > $params['rootJobTimestamp'] );
* @return string
*/
protected function getRootJobCacheKey( $signature ) {
- return $this->dupCache->makeGlobalKey(
+ return $this->wanCache->makeGlobalKey(
'jobqueue',
$this->domain,
$this->type,
use Wikimedia\Rdbms\DBConnectionError;
use Wikimedia\Rdbms\DBError;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\ScopedCallback;
/**
const MAX_JOB_RANDOM = 2147483647; // integer; 2^31 - 1, used for job_random
const MAX_OFFSET = 255; // integer; maximum number of rows to skip
- /** @var WANObjectCache */
- protected $cache;
- /** @var IDatabase|DBError|null */
+ /** @var IMaintainableDatabase|DBError|null */
protected $conn;
/** @var array|null Server configuration array */
* If not specified, the primary DB cluster for the wiki will be used.
* This can be overridden with a custom cluster so that DB handles will
* be retrieved via LBFactory::getExternalLB() and getConnection().
- * - wanCache : An instance of WANObjectCache to use for caching.
* @param array $params
*/
protected function __construct( array $params ) {
} elseif ( isset( $params['cluster'] ) && is_string( $params['cluster'] ) ) {
$this->cluster = $params['cluster'];
}
-
- $this->cache = $params['wanCache'] ?? WANObjectCache::newEmpty();
}
protected function supportedOrders() {
protected function doGetSize() {
$key = $this->getCacheKey( 'size' );
- $size = $this->cache->get( $key );
+ $size = $this->wanCache->get( $key );
if ( is_int( $size ) ) {
return $size;
}
} catch ( DBError $e ) {
throw $this->getDBException( $e );
}
- $this->cache->set( $key, $size, self::CACHE_TTL_SHORT );
+ $this->wanCache->set( $key, $size, self::CACHE_TTL_SHORT );
return $size;
}
$key = $this->getCacheKey( 'acquiredcount' );
- $count = $this->cache->get( $key );
+ $count = $this->wanCache->get( $key );
if ( is_int( $count ) ) {
return $count;
}
} catch ( DBError $e ) {
throw $this->getDBException( $e );
}
- $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+ $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
return $count;
}
$key = $this->getCacheKey( 'abandonedcount' );
- $count = $this->cache->get( $key );
+ $count = $this->wanCache->get( $key );
if ( is_int( $count ) ) {
return $count;
}
throw $this->getDBException( $e );
}
- $this->cache->set( $key, $count, self::CACHE_TTL_SHORT );
+ $this->wanCache->set( $key, $count, self::CACHE_TTL_SHORT );
return $count;
}
/** @noinspection PhpUnusedLocalVariableInspection */
$scope = $this->getScopedNoTrxFlag( $dbw );
// Check cache to see if the queue has <= OFFSET items
- $tinyQueue = $this->cache->get( $this->getCacheKey( 'small' ) );
+ $tinyQueue = $this->wanCache->get( $this->getCacheKey( 'small' ) );
$invertedDirection = false; // whether one job_random direction was already scanned
// This uses a replication safe method for acquiring jobs. One could use UPDATE+LIMIT
);
if ( !$row ) {
$tinyQueue = true; // we know the queue must have <= MAX_OFFSET rows
- $this->cache->set( $this->getCacheKey( 'small' ), 1, 30 );
+ $this->wanCache->set( $this->getCacheKey( 'small' ), 1, 30 );
continue; // use job_random
}
}
* @return bool
*/
protected function doDeduplicateRootJob( IJobSpecification $job ) {
- $params = $job->getParams();
- if ( !isset( $params['rootJobSignature'] ) ) {
- throw new MWException( "Cannot register root job; missing 'rootJobSignature'." );
- } elseif ( !isset( $params['rootJobTimestamp'] ) ) {
- throw new MWException( "Cannot register root job; missing 'rootJobTimestamp'." );
- }
- $key = $this->getRootJobCacheKey( $params['rootJobSignature'] );
- // Callers should call JobQueueGroup::push() before this method so that if the insert
- // fails, the de-duplication registration will be aborted. Since the insert is
- // deferred till "transaction idle", do the same here, so that the ordering is
+ // Callers should call JobQueueGroup::push() before this method so that if the
+ // insert fails, the de-duplication registration will be aborted. Since the insert
+ // is deferred till "transaction idle", do the same here, so that the ordering is
// maintained. Having only the de-duplication registration succeed would cause
// jobs to become no-ops without any actual jobs that made them redundant.
$dbw = $this->getMasterDB();
/** @noinspection PhpUnusedLocalVariableInspection */
$scope = $this->getScopedNoTrxFlag( $dbw );
-
- $cache = $this->dupCache;
$dbw->onTransactionCommitOrIdle(
- function () use ( $cache, $params, $key ) {
- $timestamp = $cache->get( $key ); // current last timestamp of this job
- if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
- return true; // a newer version of this root job was enqueued
- }
-
- // Update the timestamp of the last root job started at the location...
- return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+ function () use ( $job ) {
+ parent::doDeduplicateRootJob( $job );
},
__METHOD__
);
*/
protected function doFlushCaches() {
foreach ( [ 'size', 'acquiredcount' ] as $type ) {
- $this->cache->delete( $this->getCacheKey( $type ) );
+ $this->wanCache->delete( $this->getCacheKey( $type ) );
}
}
/**
* @throws JobQueueConnectionError
- * @return IDatabase
+ * @return IMaintainableDatabase
*/
protected function getMasterDB() {
try {
/**
* @param int $index (DB_REPLICA/DB_MASTER)
- * @return IDatabase
+ * @return IMaintainableDatabase
*/
protected function getDB( $index ) {
if ( $this->server ) {
? $lbFactory->getExternalLB( $this->cluster )
: $lbFactory->getMainLB( $this->domain );
- return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
+ if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
// Keep a separate connection to avoid contention and deadlocks;
// However, SQLite has the opposite behavior due to DB-level locking.
- ? $lb->getConnectionRef( $index, [], $this->domain, $lb::CONN_TRX_AUTOCOMMIT )
+ $flags = $lb::CONN_TRX_AUTOCOMMIT;
+ } else {
// Jobs insertion will be defered until the PRESEND stage to reduce contention.
- : $lb->getConnectionRef( $index, [], $this->domain );
+ $flags = 0;
+ }
+
+ return $lb->getMaintenanceConnectionRef( $index, [], $this->domain, $flags );
}
}
private function getCacheKey( $property ) {
$cluster = is_string( $this->cluster ) ? $this->cluster : 'main';
- return $this->cache->makeGlobalKey(
+ return $this->wanCache->makeGlobalKey(
'jobqueue',
$this->domain,
$cluster,
$services = MediaWikiServices::getInstance();
$conf['stats'] = $services->getStatsdDataFactory();
$conf['wanCache'] = $services->getMainWANObjectCache();
- $conf['stash'] = $services->getMainObjectStash();
return JobQueue::factory( $conf );
}
protected static $data = [];
public function __construct( array $params ) {
- parent::__construct( $params );
+ $params['wanCache'] = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
- $this->dupCache = new HashBagOStuff();
+ parent::__construct( $params );
}
/**
$conn = $this->getConnection();
try {
- $timestamp = $conn->get( $key ); // current last timestamp of this job
+ $timestamp = $conn->get( $key ); // last known timestamp of such a root job
if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
return true; // a newer version of this root job was enqueued
}
public function run() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lb = $lbFactory->getMainLB();
- $dbw = $lb->getConnection( DB_MASTER );
+ $dbw = $lb->getConnectionRef( DB_MASTER );
$this->ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
}
// Cut down on the time spent in waitForMasterPos() in the critical section
- $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
+ $dbr = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
if ( !$lb->waitForMasterPos( $dbr ) ) {
$this->setLastError( "Timed out while pre-waiting for replica DB to catch up" );
return false;
$batchSize = $wgUpdateRowsPerQuery;
$loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
- $dbw = $loadBalancer->getConnection( DB_MASTER );
- $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] );
+ $dbw = $loadBalancer->getConnectionRef( DB_MASTER );
+ $dbr = $loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
// Wait before lock to try to reduce time waiting in the lock.
if ( !$loadBalancer->waitForMasterPos( $dbr ) ) {
$lbFactory = $services->getDBLoadBalancerFactory();
$rowsPerQuery = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
- $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+ $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
$timestamp = $this->params['timestamp'] ?? null;
if ( $timestamp === null ) {
// Serialize link update job by page ID so they see each others' changes.
// The page ID and latest revision ID will be queried again after the lock
// is acquired to bail if they are changed from that of loadPageData() above.
- $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+ // Serialize links updates by page ID so they see each others' changes
+ $dbw = $lbFactory->getMainLB()->getConnectionRef( DB_MASTER );
+ /** @noinspection PhpUnusedLocalVariableInspection */
$scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' );
if ( $scopedLock === null ) {
// Another job is already updating the page, likely for a prior revision (T170596)
// Do not try to tear down any PHP output buffers
const STREAM_ALLOW_OB = 2;
+ /**
+ * Takes HTTP headers in a name => value format and converts them to the weird format
+ * expected by stream().
+ * @param string[] $headers
+ * @return array[] [ $headers, $optHeaders ]
+ * @since 1.34
+ */
+ public static function preprocessHeaders( $headers ) {
+ $rawHeaders = [];
+ $optHeaders = [];
+ foreach ( $headers as $name => $header ) {
+ $nameLower = strtolower( $name );
+ if ( in_array( $nameLower, [ 'range', 'if-modified-since' ], true ) ) {
+ $optHeaders[$nameLower] = $header;
+ } else {
+ $rawHeaders[] = "$name: $header";
+ }
+ }
+ return [ $rawHeaders, $optHeaders ];
+ }
+
/**
* @param string $path Local filesystem path to a file
* @param array $params Options map, which includes:
/**
* Get an associative array containing the item for each of the keys that have items.
- * @param string[] $keys List of keys
+ * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
* @param int $flags Bitfield; supports READ_LATEST [optional]
- * @return array Map of (key => value) for existing keys
+ * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
*/
public function getMulti( array $keys, $flags = 0 ) {
- $valuesBykey = $this->doGetMulti( $keys, $flags );
- foreach ( $valuesBykey as $key => $value ) {
+ $foundByKey = $this->doGetMulti( $keys, $flags );
+
+ $res = [];
+ foreach ( $keys as $key ) {
// Resolve one blob at a time (avoids too much I/O at once)
- $valuesBykey[$key] = $this->resolveSegments( $key, $value );
+ if ( array_key_exists( $key, $foundByKey ) ) {
+ // A value should not appear in the key if a segment is missing
+ $value = $this->resolveSegments( $key, $foundByKey[$key] );
+ if ( $value !== false ) {
+ $res[$key] = $value;
+ }
+ }
}
- return $valuesBykey;
+ return $res;
}
/**
* Get an associative array containing the item for each of the keys that have items.
* @param string[] $keys List of keys
* @param int $flags Bitfield; supports READ_LATEST [optional]
- * @return array Map of (key => value) for existing keys
+ * @return mixed[] Map of (key => value) for existing keys
*/
protected function doGetMulti( array $keys, $flags = 0 ) {
$res = [];
--- /dev/null
+# wikimedia/objectcache
+
+## Statistics
+
+Sent to StatsD under MediaWiki's namespace.
+
+### WANObjectCache
+
+The default WANObjectCache provided by MediaWikiServices disables these
+statistics in processes where `$wgCommandLineMode` is true.
+
+#### `wanobjectcache.{kClass}.{cache_action_and_result}`
+
+Call counter from `WANObjectCache::getWithSetCallback()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of:
+ * `"hit.good"`,
+ * `"hit.refresh"`,
+ * `"hit.volatile"`,
+ * `"hit.stale"`,
+ * `"miss.busy"` (or `"renew.busy"`, if the `minAsOf` is used),
+ * `"miss.compute"` (or `"renew.busy"`, if the `minAsOf` is used).
+
+#### `wanobjectcache.{kClass}.regen_set_delay`
+
+Upon cache miss, this measures the time spent in `WANObjectCache::getWithSetCallback()`,
+from the start of the method to right after the new value has been computed by the callback.
+
+This essentially measures the whole method (including retrieval of any old value,
+validation, any locks for `lockTSE`, and the callbacks), except for the time spent
+in sending the value to the backend server.
+
+* Type: Measure (in milliseconds).
+* Variable `kClass`: The first part of your cache key.
+
+#### `wanobjectcache.{kClass}.ck_touch.{result}`
+
+Call counter from `WANObjectCache::touchCheckKey()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.ck_reset.{result}`
+
+Call counter from `WANObjectCache::resetCheckKey()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.delete.{result}`
+
+Call counter from `WANObjectCache::delete()`.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+* Variable `result`: One of `"ok"` or `"error"`.
+
+#### `wanobjectcache.{kClass}.cooloff_bounce`
+
+Upon a cache miss, the `WANObjectCache::getWithSetCallback()` method generally
+recomputes the value from the callback, and stores it for re-use.
+
+If regenerating the value costs more than a certain threshold of time (e.g. 50ms),
+then for popular keys it is likely that many web servers will generate and store
+the value simultaneously when the key is entirely absent from the cache. In this case,
+the cool-off feature can be used to protect backend cache servers against network
+congestion. This protection is implemented with a lock and subsequent cool-off period.
+The winner stores their value, while other web server return their value directly.
+
+This counter is incremented whenever a new value was regenerated but not stored.
+
+* Type: Counter.
+* Variable `kClass`: The first part of your cache key.
+
+When the regeneration callback is slow, these scenarios may use the cool-off feature:
+
+* Storing the first interim value for tombstoned keys.
+
+ If a key is currently tombstoned due to a recent `delete()` action, and thus in "hold-off", then
+ the key may not be written to. A mutex lock will let one web server generate the new value and
+ (until the hold-off is over) the generated value will be considered an interim (temporary) value
+ only. Requests that cannot get the lock will use the last stored interim value.
+ If there is no interim value yet, then requests that cannot get the lock may still generate their
+ own value. Here, the cool-off feature is used to decide which requests stores their interim value.
+
+* Storing the first interim value for stale keys.
+
+ If a key is currently in "hold-off" due to a recent `touchCheckKey()` action, then the key may
+ not be written to. A mutex lock will let one web request generate the new value and (until the
+ hold-off is over) such value will be considered an interim (temporary) value only. Requests that
+ lose the lock, will instead return the last stored interim value, or (if it remained in cache) the
+ stale value preserved from before `touchCheckKey()` was called.
+ If there is no stale value and no interim value yet, then multiple requests may need to
+ generate the value simultaneously. In this case, the cool-off feature is used to decide
+ which requests store their interim value.
+
+ The same logic applies when the callback passed to getWithSetCallback() in the "touchedCallback"
+ parameter starts returning an updated timestamp due to a dependency change.
+
+* Storing the first value when `lockTSE` is used.
+
+ When `lockTSE` is in use, and no stale value is found on the backend, and no `busyValue`
+ callback is provided, then multiple requests may generate the value simultaneously;
+ the cool-off is used to decide which requests store their interim value.
if ( $ttl ) {
$result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
} else {
- // No expiry, that is very different from zero expiry in Redis
$result = $conn->set( $key, $this->serialize( $value ) );
}
} catch ( RedisException $e ) {
$this->debug( "setMulti request to $server failed" );
continue;
}
- foreach ( $batchResult as $value ) {
- if ( $value === false ) {
- $result = false;
- }
- }
+ $result = $result && !in_array( false, $batchResult, true );
} catch ( RedisException $e ) {
$this->handleException( $conn, $e );
$result = false;
$this->debug( "deleteMulti request to $server failed" );
continue;
}
- foreach ( $batchResult as $value ) {
- if ( $value === false ) {
- $result = false;
- }
- }
+ $result = $result && !in_array( false, $batchResult, true );
} catch ( RedisException $e ) {
$this->handleException( $conn, $e );
$result = false;
if ( !$conn ) {
return false;
}
- $expiry = $this->convertToRelative( $expiry );
+
+ $ttl = $this->convertToRelative( $expiry );
try {
- if ( $expiry ) {
- $result = $conn->set(
- $key,
- $this->serialize( $value ),
- [ 'nx', 'ex' => $expiry ]
- );
- } else {
- $result = $conn->setnx( $key, $this->serialize( $value ) );
- }
+ $result = $conn->set(
+ $key,
+ $this->serialize( $value ),
+ $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ]
+ );
} catch ( RedisException $e ) {
$result = false;
$this->handleException( $conn, $e );
}
$this->logRequest( 'add', $key, $server, $result );
+
return $result;
}
- /**
- * Non-atomic implementation of incr().
- *
- * Probably all callers actually want incr() to atomically initialise
- * values to zero if they don't exist, as provided by the Redis INCR
- * command. But we are constrained by the memcached-like interface to
- * return null in that case. Once the key exists, further increments are
- * atomic.
- * @param string $key Key to increase
- * @param int $value Value to add to $key (Default 1)
- * @return int|bool New value or false on failure
- */
public function incr( $key, $value = 1 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
+
try {
- if ( !$conn->exists( $key ) ) {
- return false;
+ $conn->watch( $key );
+ if ( $conn->exists( $key ) ) {
+ $conn->multi( Redis::MULTI );
+ $conn->incrBy( $key, $value );
+ $batchResult = $conn->exec();
+ if ( $batchResult === false ) {
+ $result = false;
+ } else {
+ $result = end( $batchResult );
+ }
+ } else {
+ $result = false;
+ $conn->unwatch();
}
- // @FIXME: on races, the key may have a 0 TTL
- $result = $conn->incrBy( $key, $value );
} catch ( RedisException $e ) {
+ try {
+ $conn->unwatch(); // sanity
+ } catch ( RedisException $ex ) {
+ // already errored
+ }
$result = false;
$this->handleException( $conn, $e );
}
$this->logRequest( 'incr', $key, $server, $result );
+
+ return $result;
+ }
+
+ public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ return false;
+ }
+
+ $ttl = $this->convertToRelative( $exptime );
+ $preIncrInit = $init - $value;
+ try {
+ $conn->multi( Redis::MULTI );
+ $conn->set( $key, $preIncrInit, $ttl ? [ 'nx', 'ex' => $ttl ] : [ 'nx' ] );
+ $conn->incrBy( $key, $value );
+ $batchResult = $conn->exec();
+ if ( $batchResult === false ) {
+ $result = false;
+ $this->debug( "incrWithInit request to $server failed" );
+ } else {
+ $result = end( $batchResult );
+ }
+ } catch ( RedisException $e ) {
+ $result = false;
+ $this->handleException( $conn, $e );
+ }
+
+ $this->logRequest( 'incr', $key, $server, $result );
+
return $result;
}
/** Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */
const MIN_TIMESTAMP_NONE = 0.0;
+ /** @var int One second into the UNIX timestamp epoch */
+ const EPOCH_UNIX_ONE_SECOND = 1.0;
/** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
const TINY_NEGATIVE = -0.000001;
/** Tiny positive float to use when using "minTime" to assert an inequality */
const TINY_POSTIVE = 0.000001;
- /** Milliseconds of delay after get() where set() storms are a consideration with 'lockTSE' */
+ /** Milliseconds of delay after get() where set() storms are a consideration with "lockTSE" */
const SET_DELAY_HIGH_MS = 50;
/** Min millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
const RECENT_SET_LOW_MS = 50;
/** Max millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
const RECENT_SET_HIGH_MS = 100;
+ /** @var int Seconds needed for value generation considered slow */
+ const GENERATION_SLOW_SEC = 3;
+
/** Parameter to get()/getMulti() to return extra information by reference */
const PASS_BY_REF = -1;
/** Cache format version number */
const VERSION = 1;
- const FLD_VERSION = 0; // key to cache version number
+ const FLD_FORMAT_VERSION = 0; // key to WAN cache version number
const FLD_VALUE = 1; // key to the cached value
const FLD_TTL = 2; // key to the original TTL
- const FLD_TIME = 3; // key to the cache time
+ const FLD_TIME = 3; // key to the cache timestamp
const FLD_FLAGS = 4; // key to the flags bitfield (reserved number)
- const FLD_HOLDOFF = 5; // key to any hold-off TTL
+ const FLD_VALUE_VERSION = 5; // key to collection cache version number
+ const FLD_GENERATION_TIME = 6; // key to how long it took to generate the value
+
+ const PURGE_TIME = 0; // key to the tombstone entry timestamp
+ const PURGE_HOLDOFF = 1; // key to the tombstone entry hold-off TTL
const VALUE_KEY_PREFIX = 'WANCache:v:';
const INTERIM_KEY_PREFIX = 'WANCache:i:';
const PURGE_VAL_PREFIX = 'PURGED:';
- const VFLD_DATA = 'WOC:d'; // key to the value of versioned data
- const VFLD_VERSION = 'WOC:v'; // key to the version of the value present
-
const PC_PRIMARY = 'primary:1000'; // process cache name and max key count
/**
$this->region = $params['region'] ?? 'main';
$this->cluster = $params['cluster'] ?? 'wan-main';
$this->mcrouterAware = !empty( $params['mcrouterAware'] );
- $this->epoch = $params['epoch'] ?? 1.0;
+ $this->epoch = $params['epoch'] ?? self::EPOCH_UNIX_ONE_SECOND;
$this->setLogger( $params['logger'] ?? new NullLogger() );
$this->stats = $params['stats'] ?? new NullStatsdDataFactory();
* Consider using getWithSetCallback() instead of get() and set() cycles.
* That method has cache slam avoiding features for hot/expensive keys.
*
- * Pass $info as WANObjectCache::PASS_BY_REF to transform it into a cache key info map.
+ * Pass $info as WANObjectCache::PASS_BY_REF to transform it into a cache key metadata map.
* This map includes the following metadata:
* - asOf: UNIX timestamp of the value or null if the key is nonexistant
* - tombAsOf: UNIX timestamp of the tombstone or null if the key is not tombstoned
* - lastCKPurge: UNIX timestamp of the highest check key or null if none provided
+ * - version: cached value version number or null if the key is nonexistant
*
* Otherwise, $info will transform into the cached value timestamp.
*
$info = [
'asOf' => $infoByKey[$key]['asOf'] ?? null,
'tombAsOf' => $infoByKey[$key]['tombAsOf'] ?? null,
- 'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null
+ 'lastCKPurge' => $infoByKey[$key]['lastCKPurge'] ?? null,
+ 'version' => $infoByKey[$key]['version'] ?? null
];
} else {
$info = $infoByKey[$key]['asOf'] ?? null; // b/c
* Fetch the value of several keys from cache
*
* Pass $info as WANObjectCache::PASS_BY_REF to transform it into a map of cache keys
- * to cache key info maps, each having the same style as those of WANObjectCache::get().
+ * to cache key metadata maps, each having the same style as those of WANObjectCache::get().
* All the cache keys listed in $keys will have an entry.
*
* Othwerwise, $info will transform into a map of (cache key => cached value timestamp).
// Get the main cache value for each key and validate them
foreach ( $valueKeys as $vKey ) {
$key = substr( $vKey, $vPrefixLen ); // unprefix
- list( $value, $curTTL, $asOf, $tombAsOf ) = isset( $wrappedValues[$vKey] )
- ? $this->unwrap( $wrappedValues[$vKey], $now )
- : [ false, null, null, null ]; // not found
+ list( $value, $keyInfo ) = $this->unwrap( $wrappedValues[$vKey] ?? false, $now );
// Force dependent keys to be seen as stale for a while after purging
// to reduce race conditions involving stale data getting cached
$purgeValues = $purgeValuesForAll;
$lastCKPurge = null; // timestamp of the highest check key
foreach ( $purgeValues as $purge ) {
- $lastCKPurge = max( $purge[self::FLD_TIME], $lastCKPurge );
- $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
- if ( $value !== false && $safeTimestamp >= $asOf ) {
+ $lastCKPurge = max( $purge[self::PURGE_TIME], $lastCKPurge );
+ $safeTimestamp = $purge[self::PURGE_TIME] + $purge[self::PURGE_HOLDOFF];
+ if ( $value !== false && $safeTimestamp >= $keyInfo['asOf'] ) {
// How long ago this value was invalidated by *this* check key
- $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
+ $ago = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
// How long ago this value was invalidated by *any* known check key
- $curTTL = min( $curTTL, $ago );
+ $keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
}
}
+ $keyInfo[ 'lastCKPurge'] = $lastCKPurge;
if ( $value !== false ) {
$result[$key] = $value;
}
- if ( $curTTL !== null ) {
- $curTTLs[$key] = $curTTL;
+ if ( $keyInfo['curTTL'] !== null ) {
+ $curTTLs[$key] = $keyInfo['curTTL'];
}
$infoByKey[$key] = ( $info === self::PASS_BY_REF )
- ? [ 'asOf' => $asOf, 'tombAsOf' => $tombAsOf, 'lastCKPurge' => $lastCKPurge ]
- : $asOf; // b/c
+ ? $keyInfo
+ : $keyInfo['asOf']; // b/c
}
$info = $infoByKey;
* @param mixed $value
* @param int $ttl Seconds to live. Special values are:
* - WANObjectCache::TTL_INDEFINITE: Cache forever (default)
+ * - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
* @param array $opts Options map:
- * - lag: seconds of replica DB lag. Typically, this is either the replica DB lag
+ * - lag: Seconds of replica DB lag. Typically, this is either the replica DB lag
* before the data was read or, if applicable, the replica DB lag before
* the snapshot-isolated transaction the data was read from started.
* Use false to indicate that replication is not running.
* the current time the data was read or (if applicable) the time when
* the snapshot-isolated transaction the data was read from started.
* Default: 0 seconds
- * - pending: whether this data is possibly from an uncommitted write transaction.
+ * - pending: Whether this data is possibly from an uncommitted write transaction.
* Generally, other threads should not see values from the future and
* they certainly should not see ones that ended up getting rolled back.
* Default: false
- * - lockTSE: if excessive replication/snapshot lag is detected, then store the value
+ * - lockTSE: If excessive replication/snapshot lag is detected, then store the value
* with this TTL and flag it as stale. This is only useful if the reads for this key
* use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set
* then it will still add on to this TTL in the excessive lag scenario.
* Default: WANObjectCache::TSE_NONE
- * - staleTTL: seconds to keep the key around if it is stale. The get()/getMulti()
+ * - staleTTL: Seconds to keep the key around if it is stale. The get()/getMulti()
* methods return such stale values with a $curTTL of 0, and getWithSetCallback()
* will call the regeneration callback in such cases, passing in the old value
* and its as-of time to the callback. This is useful if adaptiveTTL() is used
* on the old value's as-of time when it is verified as still being correct.
- * Default: WANObjectCache::STALE_TTL_NONE.
- * - creating: optimize for the case where the key does not already exist.
+ * Default: WANObjectCache::STALE_TTL_NONE
+ * - creating: Optimize for the case where the key does not already exist.
* Default: false
+ * - version: Integer version number signifiying the format of the value.
+ * Default: null
+ * - walltime: How long the value took to generate in seconds. Default: 0.0
* @note Options added in 1.28: staleTTL
* @note Options added in 1.33: creating
+ * @note Options added in 1.34: version, walltime
* @return bool Success
*/
final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
$now = $this->getCurrentTime();
+ $lag = $opts['lag'] ?? 0;
+ $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
+ $pending = $opts['pending'] ?? false;
$lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
$staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
- $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
$creating = $opts['creating'] ?? false;
- $lag = $opts['lag'] ?? 0;
+ $version = $opts['version'] ?? null;
+ $walltime = $opts['walltime'] ?? 0.0;
+
+ if ( $ttl < 0 ) {
+ return true;
+ }
// Do not cache potentially uncommitted data as it might get rolled back
- if ( !empty( $opts['pending'] ) ) {
+ if ( $pending ) {
$this->logger->info(
'Rejected set() for {cachekey} due to pending writes.',
[ 'cachekey' => $key ]
}
// Wrap that value with time/TTL/version metadata
- $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $now );
+ $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now, $walltime );
$storeTTL = $ttl + $staleTTL;
if ( $creating ) {
foreach ( $rawKeys as $key => $rawKey ) {
$purge = $this->parsePurgeValue( $rawValues[$rawKey] );
if ( $purge !== false ) {
- $time = $purge[self::FLD_TIME];
+ $time = $purge[self::PURGE_TIME];
} else {
// Casting assures identical floats for the next getCheckKeyTime() calls
$now = (string)$this->getCurrentTime();
$version = $opts['version'] ?? null;
$pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
- // Try the process cache if enabled and the cache callback is not within a cache callback.
- // Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
- // the in-memory value is further lagged than the shared one since it uses a blind TTL.
+ // Use the process cache if requested as long as no outer cache callback is running.
+ // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
+ // process cached values are more lagged than persistent ones as they are not purged.
if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
- $procCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
- if ( $procCache->has( $key, $pcTTL ) ) {
- return $procCache->get( $key );
+ $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
+ $cached = $pCache->get( $this->getProcessCacheKey( $key, $version ), INF, false );
+ if ( $cached !== false ) {
+ return $cached;
}
} else {
- $procCache = null;
+ $pCache = null;
}
- if ( $version !== null ) {
- $curAsOf = self::PASS_BY_REF;
- $curValue = $this->doGetWithSetCallback(
- $key,
+ $res = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
+ list( $value, $valueVersion, $curAsOf ) = $res;
+ if ( $valueVersion !== $version ) {
+ // Current value has a different version; use the variant key for this version.
+ // Regenerate the variant value if it is not newer than the main value at $key
+ // so that purges to the main key propagate to the variant value.
+ list( $value ) = $this->fetchOrRegenerate(
+ $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
$ttl,
- // Wrap the value in an array with version metadata but hide it from $callback
- function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, $version ) {
- if ( $this->isVersionedValue( $oldValue, $version ) ) {
- $oldData = $oldValue[self::VFLD_DATA];
- } else {
- // VFLD_DATA is not set if an old, unversioned, key is present
- $oldData = false;
- $oldAsOf = null;
- }
-
- return [
- self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
- self::VFLD_VERSION => $version
- ];
- },
- $opts,
- $curAsOf
+ $callback,
+ [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
);
- if ( $this->isVersionedValue( $curValue, $version ) ) {
- // Current value has the requested version; use it
- $value = $curValue[self::VFLD_DATA];
- } else {
- // Current value has a different version; use the variant key for this version.
- // Regenerate the variant value if it is not newer than the main value at $key
- // so that purges to they key propagate to the variant value.
- $value = $this->doGetWithSetCallback(
- $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
- $ttl,
- $callback,
- [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
- );
- }
- } else {
- $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
}
// Update the process cache if enabled
- if ( $procCache && $value !== false ) {
- $procCache->set( $key, $value );
+ if ( $pCache && $value !== false ) {
+ $pCache->set( $this->getProcessCacheKey( $key, $version ), $value );
}
return $value;
* @param int $ttl
* @param callable $callback
* @param array $opts Options map for getWithSetCallback()
- * @param float|null &$asOf Cache generation timestamp of returned value [returned]
- * @return mixed
+ * @return array Ordered list of the following:
+ * - Cached or regenerated value
+ * - Cached or regenerated value version number or null if not versioned
+ * - Timestamp of the cached value or null if there is no value
* @note Callable type hints are not used to avoid class-autoloading
*/
- protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
- $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
- $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
- $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
- $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
+ private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
$checkKeys = $opts['checkKeys'] ?? [];
- $busyValue = $opts['busyValue'] ?? null;
- $popWindow = $opts['hotTTR'] ?? self::HOT_TTR;
- $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
+ $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
$minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
+ $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
+ $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
+ $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
$touchedCb = $opts['touchedCallback'] ?? null;
$initialTime = $this->getCurrentTime();
$kClass = $this->determineKeyClassForStats( $key );
- // Get the current key value and metadata
+ // Get the current key value and its metadata
$curTTL = self::PASS_BY_REF;
$curInfo = self::PASS_BY_REF; /** @var array $curInfo */
$curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
// Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
- // Best possible return value and its corresponding "as of" timestamp
- $value = $curValue;
- $asOf = $curInfo['asOf'];
-
- // Determine if a cached value regeneration is needed or desired
+ // Use the cached value if it exists and is not due for synchronous regeneration
if (
- $this->isValid( $value, $asOf, $minAsOf ) &&
+ $this->isValid( $curValue, $curInfo['asOf'], $minAsOf ) &&
$this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
) {
$preemptiveRefresh = (
$this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
- $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $initialTime )
+ $this->worthRefreshPopular( $curInfo['asOf'], $ageNew, $hotTTR, $initialTime )
);
-
if ( !$preemptiveRefresh ) {
$this->stats->increment( "wanobjectcache.$kClass.hit.good" );
- return $value;
+ return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
} elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) ) {
$this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
- return $value;
+ return [ $curValue, $curInfo['version'], $curInfo['asOf'] ];
}
}
+ // Determine if there is stale or volatile cached value that is still usable
$isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
if ( $isKeyTombstoned ) {
- // Get the interim key value since the key is tombstoned (write-holed)
- list( $value, $asOf ) = $this->getInterimValue( $key, $minAsOf );
+ // Key is write-holed; use the (volatile) interim key as an alternative
+ list( $possValue, $possInfo ) = $this->getInterimValue( $key, $minAsOf );
// Update the "last purge time" since the $touchedCb timestamp depends on $value
- $LPT = $this->resolveTouched( $value, $LPT, $touchedCb );
+ $LPT = $this->resolveTouched( $possValue, $LPT, $touchedCb );
+ } else {
+ $possValue = $curValue;
+ $possInfo = $curInfo;
}
- // Reduce mutex and cache set spam while keys are in the tombstone/holdoff period by
- // checking if $value was genereated by a recent thread much less than a second ago.
+ // Avoid overhead from callback runs, regeneration locks, and cache sets during
+ // hold-off periods for the key by reusing very recently generated cached values
if (
- $this->isValid( $value, $asOf, $minAsOf, $LPT ) &&
- $this->isVolatileValueAgeNegligible( $initialTime - $asOf )
+ $this->isValid( $possValue, $possInfo['asOf'], $minAsOf, $LPT ) &&
+ $this->isVolatileValueAgeNegligible( $initialTime - $possInfo['asOf'] )
) {
$this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
- return $value;
+ return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
}
- // Decide if only one thread should handle regeneration at a time
- $useMutex =
+ $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
+ $busyValue = $opts['busyValue'] ?? null;
+ $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
+ $version = $opts['version'] ?? null;
+
+ // Determine whether one thread per datacenter should handle regeneration at a time
+ $useRegenerationLock =
// Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
// deduce the key hotness because |$curTTL| will always keep increasing until the
// tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE ) ||
// Assume a key is hot if there is no value and a busy fallback is given.
// This avoids stampedes on eviction or preemptive regeneration taking too long.
- ( $busyValue !== null && $value === false );
-
- $hasLock = false;
- if ( $useMutex ) {
- // Attempt to acquire a non-blocking lock specific to the local datacenter
- if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
- // Lock acquired; this thread will recompute the value and update cache
- $hasLock = true;
- } elseif ( $this->isValid( $value, $asOf, $minAsOf ) ) {
- // Not acquired and stale cache value exists; use the stale value
- $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
-
- return $value;
- } else {
- // Lock not acquired and no stale value exists
- if ( $busyValue !== null ) {
- // Use the busy fallback value if nothing else
+ ( $busyValue !== null && $possValue === false );
+
+ // If a regeneration lock is required, threads that do not get the lock will use any
+ // available stale or volatile value. If there is none, then the cheap/placeholder
+ // value from $busyValue will be used if provided; failing that, all threads will try
+ // to regenerate the value and ignore the lock.
+ if ( $useRegenerationLock ) {
+ $hasLock = $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL );
+ if ( !$hasLock ) {
+ if ( $this->isValid( $possValue, $possInfo['asOf'], $minAsOf ) ) {
+ $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
+
+ return [ $possValue, $possInfo['version'], $curInfo['asOf'] ];
+ } elseif ( $busyValue !== null ) {
$miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
$this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
- return is_callable( $busyValue ) ? $busyValue() : $busyValue;
+ return [
+ is_callable( $busyValue ) ? $busyValue() : $busyValue,
+ $version,
+ $curInfo['asOf']
+ ];
}
}
+ } else {
+ $hasLock = false;
}
- if ( !is_callable( $callback ) ) {
- throw new InvalidArgumentException( "Invalid cache miss callback provided." );
- }
-
- $preCallbackTime = $this->getCurrentTime();
- // Generate the new value from the callback...
+ // Generate the new value given any prior value with a matching version
$setOpts = [];
+ $preCallbackTime = $this->getCurrentTime();
++$this->callbackDepth;
try {
- $value = call_user_func_array( $callback, [ $curValue, &$ttl, &$setOpts, $asOf ] );
+ $value = $callback(
+ ( $curInfo['version'] === $version ) ? $curValue : false,
+ $ttl,
+ $setOpts,
+ ( $curInfo['version'] === $version ) ? $curInfo['asOf'] : null
+ );
} finally {
--$this->callbackDepth;
}
- $valueIsCacheable = ( $value !== false && $ttl >= 0 );
+ $postCallbackTime = $this->getCurrentTime();
- if ( $valueIsCacheable ) {
- $ago = max( $this->getCurrentTime() - $initialTime, 0.0 );
- $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $ago );
+ // How long it took to fetch, validate, and generate the value
+ $elapsed = max( $postCallbackTime - $initialTime, 0.0 );
+ // Attempt to save the newly generated value if applicable
+ if (
+ // Callback yielded a cacheable value
+ ( $value !== false && $ttl >= 0 ) &&
+ // Current thread was not raced out of a regeneration lock or key is tombstoned
+ ( !$useRegenerationLock || $hasLock || $isKeyTombstoned ) &&
+ // Key does not appear to be undergoing a set() stampede
+ $this->checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock )
+ ) {
+ // How long it took to generate the value
+ $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
+ // If the key is write-holed then use the (volatile) interim key as an alternative
if ( $isKeyTombstoned ) {
- if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
- // Use the interim key value since the key is tombstoned (write-holed)
- $tempTTL = max( self::INTERIM_KEY_TTL, (int)$lockTSE );
- $this->setInterimValue( $key, $value, $tempTTL, $this->getCurrentTime() );
- }
- } elseif ( !$useMutex || $hasLock ) {
- if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
- $setOpts['creating'] = ( $curValue === false );
- // Save the value unless a lock-winning thread is already expected to do that
- $setOpts['lockTSE'] = $lockTSE;
- $setOpts['staleTTL'] = $staleTTL;
- // Use best known "since" timestamp if not provided
- $setOpts += [ 'since' => $preCallbackTime ];
- // Update the cache; this will fail if the key is tombstoned
- $this->set( $key, $value, $ttl, $setOpts );
- }
+ $this->setInterimValue( $key, $value, $lockTSE, $version, $walltime );
+ } else {
+ $finalSetOpts = [
+ 'since' => $setOpts['since'] ?? $preCallbackTime,
+ 'version' => $version,
+ 'staleTTL' => $staleTTL,
+ 'lockTSE' => $lockTSE, // informs lag vs performance trade-offs
+ 'creating' => ( $curValue === false ), // optimization
+ 'walltime' => $walltime
+ ] + $setOpts;
+ $this->set( $key, $value, $ttl, $finalSetOpts );
}
}
$miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
$this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
- return $value;
+ return [ $value, $version, $curInfo['asOf'] ];
}
/**
* @return bool Whether it is OK to proceed with a key set operation
*/
private function checkAndSetCooloff( $key, $kClass, $elapsed, $lockTSE, $hasLock ) {
+ $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $elapsed );
+
// If $lockTSE is set, the lock was bypassed because there was no stale/interim value,
// and $elapsed indicates that regeration is slow, then there is a risk of set()
// stampedes with large blobs. With a typical scale-out infrastructure, CPU and query
* @return array (current time left or null, UNIX timestamp of last purge or null)
* @note Callable type hints are not used to avoid class-autoloading
*/
- protected function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
+ private function resolveCTL( $value, $curTTL, $curInfo, $touchedCallback ) {
if ( $touchedCallback === null || $value === false ) {
return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'] ) ];
}
- if ( !is_callable( $touchedCallback ) ) {
- throw new InvalidArgumentException( "Invalid expiration callback provided." );
- }
-
$touched = $touchedCallback( $value );
if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
$curTTL = min( $curTTL, self::TINY_NEGATIVE, $curInfo['asOf'] - $touched );
* @return float|null UNIX timestamp of last purge or null
* @note Callable type hints are not used to avoid class-autoloading
*/
- protected function resolveTouched( $value, $lastPurge, $touchedCallback ) {
- if ( $touchedCallback === null || $value === false ) {
- return $lastPurge;
- }
-
- if ( !is_callable( $touchedCallback ) ) {
- throw new InvalidArgumentException( "Invalid expiration callback provided." );
- }
-
- return max( $touchedCallback( $value ), $lastPurge );
+ private function resolveTouched( $value, $lastPurge, $touchedCallback ) {
+ return ( $touchedCallback === null || $value === false )
+ ? $lastPurge // nothing to derive the "touched timestamp" from
+ : max( $touchedCallback( $value ), $lastPurge );
}
/**
* @param string $key
* @param float $minAsOf Minimum acceptable "as of" timestamp
- * @return array (cached value or false, cached value timestamp or null)
+ * @return array (cached value or false, cache key metadata map)
*/
- protected function getInterimValue( $key, $minAsOf ) {
- if ( !$this->useInterimHoldOffCaching ) {
- return [ false, null ]; // disabled
- }
+ private function getInterimValue( $key, $minAsOf ) {
+ $now = $this->getCurrentTime();
+
+ if ( $this->useInterimHoldOffCaching ) {
+ $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
- $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
- list( $value ) = $this->unwrap( $wrapped, $this->getCurrentTime() );
- $valueAsOf = $wrapped[self::FLD_TIME] ?? null;
- if ( $this->isValid( $value, $valueAsOf, $minAsOf ) ) {
- return [ $value, $valueAsOf ];
+ list( $value, $keyInfo ) = $this->unwrap( $wrapped, $now );
+ if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
+ return [ $value, $keyInfo ];
+ }
}
- return [ false, null ];
+ return $this->unwrap( false, $now );
}
/**
* @param string $key
* @param mixed $value
- * @param int $tempTTL
- * @param float $newAsOf
+ * @param int $ttl
+ * @param int|null $version Value version number
+ * @param float $walltime How long it took to generate the value in seconds
*/
- protected function setInterimValue( $key, $value, $tempTTL, $newAsOf ) {
- $wrapped = $this->wrap( $value, $tempTTL, $newAsOf );
+ private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
+ $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
+ $wrapped = $this->wrap( $value, $ttl, $version, $this->getCurrentTime(), $walltime );
$this->cache->merge(
self::INTERIM_KEY_PREFIX . $key,
function () use ( $wrapped ) {
return $wrapped;
},
- $tempTTL,
+ $ttl,
1
);
}
ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
) {
$valueKeys = array_keys( $keyedIds->getArrayCopy() );
- $checkKeys = $opts['checkKeys'] ?? [];
- $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
// Load required keys into process cache in one go
$this->warmupCache = $this->getRawKeysForWarmup(
- $this->getNonProcessCachedKeys( $valueKeys, $opts, $pcTTL ),
- $checkKeys
+ $this->getNonProcessCachedKeys( $valueKeys, $opts ),
+ $opts['checkKeys'] ?? []
);
$this->warmupKeyMisses = 0;
$idsByValueKey = $keyedIds->getArrayCopy();
$valueKeys = array_keys( $idsByValueKey );
$checkKeys = $opts['checkKeys'] ?? [];
- $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
unset( $opts['lockTSE'] ); // incompatible
unset( $opts['busyValue'] ); // incompatible
// Load required keys into process cache in one go
- $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts, $pcTTL );
+ $keysGet = $this->getNonProcessCachedKeys( $valueKeys, $opts );
$this->warmupCache = $this->getRawKeysForWarmup( $keysGet, $checkKeys );
$this->warmupKeyMisses = 0;
*/
final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
$purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
- if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
+ if ( $purge && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
$isStale = true;
$this->logger->warning( "Reaping stale check key '$key'." );
$ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, self::TTL_SECOND );
* This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
*
* @param string $key Cache key
- * @param int $ttl How long to keep the tombstone [seconds]
+ * @param int $ttl Seconds to keep the tombstone around
* @param int $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
* @return bool Success
*/
/**
* @param string $key
- * @param int $ttl
+ * @param int $ttl Seconds to live
* @param callable $callback
* @param array $opts
* @return bool Success
+ * @note Callable type hints are not used to avoid class-autoloading
*/
private function scheduleAsyncRefresh( $key, $ttl, $callback, $opts ) {
if ( !$this->asyncHandler ) {
$func = $this->asyncHandler;
$func( function () use ( $key, $ttl, $callback, $opts ) {
$opts['minAsOf'] = INF; // force a refresh
- $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
+ $this->fetchOrRegenerate( $key, $ttl, $callback, $opts );
} );
return true;
* @param int $graceTTL Consider using stale values if $curTTL is greater than this
* @return bool
*/
- protected function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
+ private function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
if ( $curTTL > 0 ) {
return true;
} elseif ( $graceTTL <= 0 ) {
}
/**
- * Do not use this method outside WANObjectCache
- *
* @param mixed $value
- * @param int $ttl [0=forever]
+ * @param int $ttl Seconds to live or zero for "indefinite"
+ * @param int|null $version Value version number or null if not versioned
* @param float $now Unix Current timestamp just before calling set()
+ * @param float $walltime How long it took to generate the value in seconds
* @return array
*/
- protected function wrap( $value, $ttl, $now ) {
- return [
- self::FLD_VERSION => self::VERSION,
+ private function wrap( $value, $ttl, $version, $now, $walltime ) {
+ // Returns keys in ascending integer order for PHP7 array packing:
+ // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
+ $wrapped = [
+ self::FLD_FORMAT_VERSION => self::VERSION,
self::FLD_VALUE => $value,
self::FLD_TTL => $ttl,
self::FLD_TIME => $now
];
+ if ( $version !== null ) {
+ $wrapped[self::FLD_VALUE_VERSION] = $version;
+ }
+ if ( $walltime >= self::GENERATION_SLOW_SEC ) {
+ $wrapped[self::FLD_GENERATION_TIME] = $walltime;
+ }
+
+ return $wrapped;
}
/**
- * Do not use this method outside WANObjectCache
- *
- * The cached value will be false if absent/tombstoned/malformed
- *
- * @param array|string|bool $wrapped
+ * @param array|string|bool $wrapped The entry at a cache key
* @param float $now Unix Current timestamp (preferrably pre-query)
- * @return array (cached value or false, current TTL, value timestamp, tombstone timestamp)
+ * @return array (value or false if absent/tombstoned/malformed, value metadata map).
+ * The cache key metadata includes the following metadata:
+ * - asOf: UNIX timestamp of the value or null if there is no value
+ * - curTTL: remaining time-to-live (negative if tombstoned) or null if there is no value
+ * - version: value version number or null if the if there is no value
+ * - tombAsOf: UNIX timestamp of the tombstone or null if there is no tombstone
*/
- protected function unwrap( $wrapped, $now ) {
- // Check if the value is a tombstone
- $purge = $this->parsePurgeValue( $wrapped );
- if ( $purge !== false ) {
- // Purged values should always have a negative current $ttl
- $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
- return [ false, $curTTL, null, $purge[self::FLD_TIME] ];
- }
-
- if ( !is_array( $wrapped ) // not found
- || !isset( $wrapped[self::FLD_VERSION] ) // wrong format
- || $wrapped[self::FLD_VERSION] !== self::VERSION // wrong version
- ) {
- return [ false, null, null, null ];
- }
-
- if ( $wrapped[self::FLD_TTL] > 0 ) {
- // Get the approximate time left on the key
- $age = $now - $wrapped[self::FLD_TIME];
- $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
+ private function unwrap( $wrapped, $now ) {
+ $value = false;
+ $info = [ 'asOf' => null, 'curTTL' => null, 'version' => null, 'tombAsOf' => null ];
+
+ if ( is_array( $wrapped ) ) {
+ // Entry expected to be a cached value; validate it
+ if (
+ ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
+ $wrapped[self::FLD_TIME] >= $this->epoch
+ ) {
+ if ( $wrapped[self::FLD_TTL] > 0 ) {
+ // Get the approximate time left on the key
+ $age = $now - $wrapped[self::FLD_TIME];
+ $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
+ } else {
+ // Key had no TTL, so the time left is unbounded
+ $curTTL = INF;
+ }
+ $value = $wrapped[self::FLD_VALUE];
+ $info['version'] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
+ $info['asOf'] = $wrapped[self::FLD_TIME];
+ $info['curTTL'] = $curTTL;
+ }
} else {
- // Key had no TTL, so the time left is unbounded
- $curTTL = INF;
- }
-
- if ( $wrapped[self::FLD_TIME] < $this->epoch ) {
- // Values this old are ignored
- return [ false, null, null, null ];
+ // Entry expected to be a tombstone; parse it
+ $purge = $this->parsePurgeValue( $wrapped );
+ if ( $purge !== false ) {
+ // Tombstoned keys should always have a negative current $ttl
+ $info['curTTL'] = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
+ $info['tombAsOf'] = $purge[self::PURGE_TIME];
+ }
}
- return [ $wrapped[self::FLD_VALUE], $curTTL, $wrapped[self::FLD_TIME], null ];
+ return [ $value, $info ];
}
/**
* @param string $key String of the format <scope>:<class>[:<class or variable>]...
* @return string A collection name to describe this class of key
*/
- protected function determineKeyClassForStats( $key ) {
+ private function determineKeyClassForStats( $key ) {
$parts = explode( ':', $key, 3 );
return $parts[1] ?? $parts[0]; // sanity
* @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
* or false if value isn't a valid purge value
*/
- protected function parsePurgeValue( $value ) {
+ private function parsePurgeValue( $value ) {
if ( !is_string( $value ) ) {
return false;
}
}
return [
- self::FLD_TIME => (float)$segments[1],
- self::FLD_HOLDOFF => (int)$segments[2],
+ self::PURGE_TIME => (float)$segments[1],
+ self::PURGE_HOLDOFF => (int)$segments[2],
];
}
* @param int $holdoff In seconds
* @return string Wrapped purge value
*/
- protected function makePurgeValue( $timestamp, $holdoff ) {
+ private function makePurgeValue( $timestamp, $holdoff ) {
return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
}
- /**
- * @param mixed $value
- * @param int $version
- * @return bool
- */
- protected function isVersionedValue( $value, $version ) {
- return (
- is_array( $value ) &&
- array_key_exists( self::VFLD_DATA, $value ) &&
- array_key_exists( self::VFLD_VERSION, $value ) &&
- $value[self::VFLD_VERSION] === $version
- );
- }
-
/**
* @param string $group
* @return MapCacheLRU
*/
- protected function getProcessCache( $group ) {
+ private function getProcessCache( $group ) {
if ( !isset( $this->processCaches[$group] ) ) {
- list( , $n ) = explode( ':', $group );
- $this->processCaches[$group] = new MapCacheLRU( (int)$n );
+ list( , $size ) = explode( ':', $group );
+ $this->processCaches[$group] = new MapCacheLRU( (int)$size );
}
return $this->processCaches[$group];
}
+ /**
+ * @param string $key
+ * @param int $version
+ * @return string
+ */
+ private function getProcessCacheKey( $key, $version ) {
+ return $key . ' ' . (int)$version;
+ }
+
/**
* @param array $keys
* @param array $opts
- * @param int $pcTTL
- * @return array List of keys
+ * @return string[] List of keys
*/
- private function getNonProcessCachedKeys( array $keys, array $opts, $pcTTL ) {
+ private function getNonProcessCachedKeys( array $keys, array $opts ) {
+ $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
+
$keysFound = [];
- if ( isset( $opts['pcTTL'] ) && $opts['pcTTL'] > 0 && $this->callbackDepth == 0 ) {
- $pcGroup = $opts['pcGroup'] ?? self::PC_PRIMARY;
- $procCache = $this->getProcessCache( $pcGroup );
+ if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
+ $version = $opts['version'] ?? null;
+ $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
foreach ( $keys as $key ) {
- if ( $procCache->has( $key, $pcTTL ) ) {
+ if ( $pCache->has( $this->getProcessCacheKey( $key, $version ), $pcTTL ) ) {
$keysFound[] = $key;
}
}
$masterName = $lb->getServerName( $lb->getWriterIndex() );
if ( $lb->hasStreamingReplicaServers() ) {
- $pos = $lb->getMasterPos();
+ $pos = $lb->getReplicaResumePos();
if ( $pos ) {
$this->logger->debug( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
$this->shutdownPositions[$masterName] = $pos;
use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
use Wikimedia\Timestamp\ConvertibleTimestamp;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
use BagOStuff;
use HashBagOStuff;
use LogicException;
/** @var int Writes to this temporary table effect lastDoneWrites() */
private static $TEMP_PSEUDO_PERMANENT = 2;
- /** Number of times to re-try an operation in case of deadlock */
+ /** @var int Number of times to re-try an operation in case of deadlock */
private static $DEADLOCK_TRIES = 4;
- /** Minimum time to wait before retry, in microseconds */
+ /** @var int Minimum time to wait before retry, in microseconds */
private static $DEADLOCK_DELAY_MIN = 500000;
- /** Maximum time to wait before retry */
+ /** @var int Maximum time to wait before retry */
private static $DEADLOCK_DELAY_MAX = 1500000;
- /** How long before it is worth doing a dummy query to test the connection */
+ /** @var int How long before it is worth doing a dummy query to test the connection */
private static $PING_TTL = 1.0;
+ /** @var string Dummy SQL query */
private static $PING_QUERY = 'SELECT 1 AS ping';
+ /** @var float Guess of how many seconds it takes to replicate a small insert */
private static $TINY_WRITE_SEC = 0.010;
+ /** @var float Consider a write slow if it took more than this many seconds */
private static $SLOW_WRITE_SEC = 0.500;
+ /** @var float Assume an insert of this many rows or less should be fast to replicate */
private static $SMALL_WRITE_ROWS = 100;
/**
/**
* Open a new connection to the database (closing any existing one)
*
- * @param string $server Database server host
- * @param string $user Database user name
- * @param string $password Database user password
- * @param string $dbName Database name
+ * @param string|null $server Database server host
+ * @param string|null $user Database user name
+ * @param string|null $password Database user password
+ * @param string|null $dbName Database name
* @param string|null $schema Database schema name
* @param string $tablePrefix Table prefix
* @throws DBConnectionError
*
* This also connects to the database immediately upon object construction
*
- * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
- * @param array $p Parameter map with keys:
+ * @param string $type A possible DB type (sqlite, mysql, postgres,...)
+ * @param array $params Parameter map with keys:
* - host : The hostname of the DB server
* - user : The name of the database user the client operates under
* - password : The password for the database user
* @throws InvalidArgumentException If the database driver or extension cannot be found
* @since 1.18
*/
- final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
- $class = self::getClass( $dbType, $p['driver'] ?? null );
+ final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
+ $class = self::getClass( $type, $params['driver'] ?? null );
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
- // Resolve some defaults for b/c
- $p['host'] = $p['host'] ?? false;
- $p['user'] = $p['user'] ?? false;
- $p['password'] = $p['password'] ?? false;
- $p['dbname'] = $p['dbname'] ?? false;
- $p['flags'] = $p['flags'] ?? 0;
- $p['variables'] = $p['variables'] ?? [];
- $p['tablePrefix'] = $p['tablePrefix'] ?? '';
- $p['schema'] = $p['schema'] ?? null;
- $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
- $p['agent'] = $p['agent'] ?? '';
- if ( !isset( $p['connLogger'] ) ) {
- $p['connLogger'] = new NullLogger();
- }
- if ( !isset( $p['queryLogger'] ) ) {
- $p['queryLogger'] = new NullLogger();
- }
- $p['profiler'] = $p['profiler'] ?? null;
- if ( !isset( $p['trxProfiler'] ) ) {
- $p['trxProfiler'] = new TransactionProfiler();
- }
- if ( !isset( $p['errorLogger'] ) ) {
- $p['errorLogger'] = function ( Exception $e ) {
+ $params += [
+ 'host' => null,
+ 'user' => null,
+ 'password' => null,
+ 'dbname' => null,
+ 'schema' => null,
+ 'tablePrefix' => '',
+ 'flags' => 0,
+ 'variables' => [],
+ 'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
+ 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+ ];
+
+ $normalizedParams = [
+ // Configuration
+ 'host' => strlen( $params['host'] ) ? $params['host'] : null,
+ 'user' => strlen( $params['user'] ) ? $params['user'] : null,
+ 'password' => is_string( $params['password'] ) ? $params['password'] : null,
+ 'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
+ 'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
+ 'tablePrefix' => (string)$params['tablePrefix'],
+ 'flags' => (int)$params['flags'],
+ 'variables' => $params['variables'],
+ 'cliMode' => (bool)$params['cliMode'],
+ 'agent' => (string)$params['agent'],
+ // Objects and callbacks
+ 'profiler' => $params['profiler'] ?? null,
+ 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
+ 'connLogger' => $params['connLogger'] ?? new NullLogger(),
+ 'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
+ 'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
- };
- }
- if ( !isset( $p['deprecationLogger'] ) ) {
- $p['deprecationLogger'] = function ( $msg ) {
+ },
+ 'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
trigger_error( $msg, E_USER_DEPRECATED );
- };
- }
+ }
+ ] + $params;
/** @var Database $conn */
- $conn = new $class( $p );
- if ( $connect == self::NEW_CONNECTED ) {
+ $conn = new $class( $normalizedParams );
+ if ( $connect === self::NEW_CONNECTED ) {
$conn->initConnection();
}
} else {
$fname = false,
callable $inputCallback = null
) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$fp = fopen( $filename, 'r' );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $fp === false ) {
throw new RuntimeException( "Could not open \"{$filename}\"" );
if ( $this->conn ) {
// Avoid connection leaks for sanity. Normally, resources close at script completion.
// The connection might already be closed in zend/hhvm by now, so suppress warnings.
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$this->closeConnection();
- Wikimedia\restoreWarnings();
- $this->conn = false;
+ AtEase::restoreWarnings();
+ $this->conn = null;
}
}
}
use DateTime;
use DateTimeZone;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
use InvalidArgumentException;
use Exception;
use RuntimeException;
* @throws DBUnexpectedError
*/
public function freeResult( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$ok = $this->mysqlFreeResult( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( !$ok ) {
throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
}
* @throws DBUnexpectedError
*/
public function fetchObject( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$row = $this->mysqlFetchObject( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
$errno = $this->lastErrno();
// Unfortunately, mysql_fetch_object does not reset the last errno.
* @throws DBUnexpectedError
*/
public function fetchRow( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$row = $this->mysqlFetchArray( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
$errno = $this->lastErrno();
// Unfortunately, mysql_fetch_array does not reset the last errno.
if ( is_bool( $res ) ) {
$n = 0;
} else {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$n = $this->mysqlNumRows( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
}
// Unfortunately, mysql_num_rows does not reset the last errno.
public function lastError() {
if ( $this->conn ) {
# Even if it's non-zero, it can still be invalid
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$error = $this->mysqlError( $this->conn );
if ( !$error ) {
$error = $this->mysqlError();
}
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
} else {
$error = $this->mysqlError();
}
use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia\WaitConditionLoop;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
use Exception;
/**
}
public function freeResult( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$ok = pg_free_result( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( !$ok ) {
throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
}
}
public function fetchObject( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$row = pg_fetch_object( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
# @todo FIXME: HACK HACK HACK HACK debug
# @todo hashar: not sure if the following test really trigger if the object
}
public function fetchRow( $res ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$row = pg_fetch_array( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
$conn = $this->getBindingHandle();
if ( pg_last_error( $conn ) ) {
return 0;
}
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$n = pg_num_rows( ResultWrapper::unwrap( $res ) );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
$conn = $this->getBindingHandle();
if ( pg_last_error( $conn ) ) {
}
/**
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname Calling function name
+ * @return string[]
* @suppress SecurityCheck-SQLInjection array_map not recognized T204911
*/
- public function listTables( $prefix = null, $fname = __METHOD__ ) {
+ public function listTables( $prefix = '', $fname = __METHOD__ ) {
$eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) );
$result = $this->query(
"SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", $fname );
foreach ( $result as $table ) {
$vars = get_object_vars( $table );
$table = array_pop( $vars );
- if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ if ( $prefix == '' || strpos( $table, $prefix ) === 0 ) {
$endArray[] = $table;
}
}
// time needed to wait on the next clusters.
$masterPositions = array_fill( 0, count( $lbs ), false );
foreach ( $lbs as $i => $lb ) {
- if ( !$lb->hasStreamingReplicaServers() ) {
- continue; // T29975: no replication; avoid getMasterPos() permissions errors
- } elseif (
- $opts['ifWritesSince'] &&
- $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
+ if (
+ // No writes to wait on getting replicated
+ !$lb->hasMasterConnection() ||
+ // No replication; avoid getMasterPos() permissions errors (T29975)
+ !$lb->hasStreamingReplicaServers() ||
+ // No writes since the last replication wait
+ (
+ $opts['ifWritesSince'] &&
+ $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
+ )
) {
- continue; // no writes since the last wait
+ continue; // no need to wait
}
+
$masterPositions[$i] = $lb->getMasterPos();
}
public function getServerAttributes( $i );
/**
- * Get the current master position for chronology control purposes
+ * Get the current master replication position
+ *
* @return DBMasterPos|bool Returns false if not applicable
+ * @throws DBError
*/
public function getMasterPos();
+ /**
+ * Get the highest DB replication position for chronology control purposes
+ *
+ * If there is only a master server then this returns false. If replication is present
+ * and correctly configured, then this returns the highest replication position of any
+ * server with an open connection. That position can later be passed to waitFor() on a
+ * new load balancer instance to make sure that queries on the new connections see data
+ * at least as up-to-date as queries (prior to this method call) on the old connections.
+ *
+ * This can be useful for implementing session consistency, where the session
+ * will be resumed accross multiple HTTP requests or CLI script instances.
+ *
+ * @return DBMasterPos|bool Replication position or false if not applicable
+ * @since 1.34
+ */
+ public function getReplicaResumePos();
+
/**
* Disable this load balancer. All connections are closed, and any attempt to
* open a new connection will result in a DBAccessError.
}
public function getMasterPos() {
- # If this entire request was served from a replica DB without opening a connection to the
- # master (however unlikely that may be), then we can fetch the position from the replica DB.
+ $index = $this->getWriterIndex();
+
+ $conn = $this->getAnyOpenConnection( $index );
+ if ( $conn ) {
+ return $conn->getMasterPos();
+ }
+
+ $conn = $this->getConnection( $index, self::CONN_SILENCE_ERRORS );
+ if ( !$conn ) {
+ $this->reportConnectionError();
+ return null; // unreachable due to exception
+ }
+
+ try {
+ $pos = $conn->getMasterPos();
+ } finally {
+ $this->closeConnection( $conn );
+ }
+
+ return $pos;
+ }
+
+ public function getReplicaResumePos() {
+ // Get the position of any existing master server connection
$masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
- if ( !$masterConn ) {
- $serverCount = $this->getServerCount();
- for ( $i = 1; $i < $serverCount; $i++ ) {
- $conn = $this->getAnyOpenConnection( $i );
- if ( $conn ) {
- return $conn->getReplicaPos();
- }
- }
- } else {
+ if ( $masterConn ) {
return $masterConn->getMasterPos();
}
- return false;
+ // Get the highest position of any existing replica server connection
+ $highestPos = false;
+ $serverCount = $this->getServerCount();
+ for ( $i = 1; $i < $serverCount; $i++ ) {
+ if ( !empty( $this->servers[$i]['is static'] ) ) {
+ continue; // server does not use replication
+ }
+
+ $conn = $this->getAnyOpenConnection( $i );
+ $pos = $conn ? $conn->getReplicaPos() : false;
+ if ( !$pos ) {
+ continue; // no open connection or could not get position
+ }
+
+ $highestPos = $highestPos ?: $pos;
+ if ( $pos->hasReached( $highestPos ) ) {
+ $highestPos = $pos;
+ }
+ }
+
+ return $highestPos;
}
public function disable() {
* @deprecated Since 1.28 Use MediaWikiServices::getInstance()->getMainWANObjectCache()
*/
public static function getMainWANInstance() {
+ wfDeprecated( __METHOD__, '1.28' );
return MediaWikiServices::getInstance()->getMainWANObjectCache();
}
* @throws MWException
*/
protected function getDB( $serverIndex ) {
- if ( !isset( $this->conns[$serverIndex] ) ) {
- if ( $serverIndex >= $this->numServers ) {
- throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
- }
+ if ( $serverIndex >= $this->numServers ) {
+ throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+ }
- # Don't keep timing out trying to connect for each call if the DB is down
- if ( isset( $this->connFailureErrors[$serverIndex] )
- && ( time() - $this->connFailureTimes[$serverIndex] ) < 60
- ) {
- throw $this->connFailureErrors[$serverIndex];
- }
+ # Don't keep timing out trying to connect for each call if the DB is down
+ if (
+ isset( $this->connFailureErrors[$serverIndex] ) &&
+ ( time() - $this->connFailureTimes[$serverIndex] ) < 60
+ ) {
+ throw $this->connFailureErrors[$serverIndex];
+ }
- if ( $this->serverInfos ) {
+ if ( $this->serverInfos ) {
+ if ( !isset( $this->conns[$serverIndex] ) ) {
// Use custom database defined by server connection info
$info = $this->serverInfos[$serverIndex];
$type = $info['type'] ?? 'mysql';
$this->logger->debug( __CLASS__ . ": connecting to $host" );
$db = Database::factory( $type, $info );
$db->clearFlag( DBO_TRX ); // auto-commit mode
+ $this->conns[$serverIndex] = $db;
+ }
+ $db = $this->conns[$serverIndex];
+ } else {
+ // Use the main LB database
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
+ if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
+ // Keep a separate connection to avoid contention and deadlocks
+ $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
} else {
- // Use the main LB database
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
- if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
- // Keep a separate connection to avoid contention and deadlocks
- $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
- } else {
- // However, SQLite has the opposite behavior due to DB-level locking.
- // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
- $db = $lb->getConnection( $index );
- }
+ // However, SQLite has the opposite behavior due to DB-level locking.
+ // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
+ $db = $lb->getConnection( $index );
}
-
- $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
- $this->conns[$serverIndex] = $db;
}
- return $this->conns[$serverIndex];
+ $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
+
+ return $db;
}
/**
# Show delete and move logs if there were any such events.
# The logging query can DOS the site when bots/crawlers cause 404 floods,
# so be careful showing this. 404 pages must be cheap as they are hard to cache.
- $cache = $services->getMainObjectStash();
- $key = $cache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
+ $dbCache = ObjectCache::getInstance( 'db-replicated' );
+ $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
$loggedIn = $this->getContext()->getUser()->isLoggedIn();
$sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
- if ( $loggedIn || $cache->get( $key ) || $sessionExists ) {
+ if ( $loggedIn || $dbCache->get( $key ) || $sessionExists ) {
$logTypes = [ 'delete', 'move', 'protect' ];
$dbr = wfGetDB( DB_REPLICA );
$baseRevId = null;
if ( $edittime && $sectionId !== 'new' ) {
$lb = $this->getDBLoadBalancer();
- $dbr = $lb->getConnection( DB_REPLICA );
+ $dbr = $lb->getConnectionRef( DB_REPLICA );
$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
// Try the master if this thread may have just added it.
// This could be abstracted into a Revision method, but we don't want
&& $lb->getServerCount() > 1
&& $lb->hasOrMadeRecentMasterChanges()
) {
- $dbw = $lb->getConnection( DB_MASTER );
+ $dbw = $lb->getConnectionRef( DB_MASTER );
$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
}
if ( $rev ) {
$status->value = $logid;
// Show log excerpt on 404 pages rather than just a link
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
- $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
- $cache->set( $key, 1, $cache::TTL_DAY );
+ $dbCache = ObjectCache::getInstance( 'db-replicated' );
+ $key = $dbCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+ $dbCache->set( $key, 1, $dbCache::TTL_DAY );
}
return $status;
protected $type = 'generic';
/** @var bool */
protected $cacheable = false; // does this override getCachedWork() ?
+ /** @var PoolCounter */
+ private $poolCounter;
/**
* @param string $type The class of actions to limit concurrency for (task type)
* @param string $text
*/
function update( $id, $title, $text ) {
- $dbw = $this->lb->getConnection( DB_MASTER );
+ $dbw = $this->lb->getMaintenanceConnectionRef( DB_MASTER );
$dbw->replace( 'searchindex',
[ 'si_page' ],
[
* Whether fulltext search is supported by current schema
* @return bool
*/
- function fulltextSearchSupported() {
+ private function fulltextSearchSupported() {
+ // Avoid getConnectionRef() in order to get DatabaseSqlite specifically
/** @var DatabaseSqlite $dbr */
$dbr = $this->lb->getConnection( DB_REPLICA );
-
- return $dbr->checkForEnabledSearch();
+ try {
+ return $dbr->checkForEnabledSearch();
+ } finally {
+ $this->lb->reuseConnection( $dbr );
+ }
}
/**
* @param string $title
* @param string $text
*/
- function update( $id, $title, $text ) {
+ public function update( $id, $title, $text ) {
if ( !$this->fulltextSearchSupported() ) {
return;
}
* @param int $id
* @param string $title
*/
- function updateTitle( $id, $title ) {
+ public function updateTitle( $id, $title ) {
if ( !$this->fulltextSearchSupported() ) {
return;
}
protected function loadSites() {
$this->sites = new SiteList();
- $dbr = $this->dbLoadBalancer->getConnection( DB_REPLICA );
+ $dbr = $this->dbLoadBalancer->getConnectionRef( DB_REPLICA );
$res = $dbr->select(
'sites',
return true;
}
- $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
* @return bool Success
*/
public function clear() {
- $dbw = $this->dbLoadBalancer->getConnection( DB_MASTER );
+ $dbw = $this->dbLoadBalancer->getConnectionRef( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$ok = $dbw->delete( 'sites', '*', __METHOD__ );
return $params;
}
- public function onAuthChangeFormFields(
- array $requests, array $fieldInfo, array &$formDescriptor, $action
- ) {
- // This method is never called for remove actions.
-
- $extraFields = [];
- Hooks::run( 'ChangePasswordForm', [ &$extraFields ], '1.27' );
- foreach ( $extraFields as $extra ) {
- list( $name, $label, $type, $default ) = $extra;
- $formDescriptor[$name] = [
- 'type' => $type,
- 'name' => $name,
- 'label-message' => $label,
- 'default' => $default,
- ];
-
- }
-
- return parent::onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
- }
-
public function execute( $subPage ) {
$this->setHeaders();
$this->outputHeader();
* @file
* @ingroup Upload
*/
-use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
/**
protected $mTempPath;
/** @var TempFSFile|null Wrapper to handle deleting the temp file */
protected $tempFileObj;
-
- protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
- protected $mTitle = false, $mTitleError = 0;
- protected $mFilteredName, $mFinalExtension;
- protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps;
+ /** @var string|null */
+ protected $mDesiredDestName;
+ /** @var string|null */
+ protected $mDestName;
+ /** @var string|null */
+ protected $mRemoveTempFile;
+ /** @var string|null */
+ protected $mSourceType;
+ /** @var Title|bool */
+ protected $mTitle = false;
+ /** @var int */
+ protected $mTitleError = 0;
+ /** @var string|null */
+ protected $mFilteredName;
+ /** @var string|null */
+ protected $mFinalExtension;
+ /** @var LocalFile */
+ protected $mLocalFile;
+ /** @var UploadStashFile */
+ protected $mStashFile;
+ /** @var int|null */
+ protected $mFileSize;
+ /** @var array|null */
+ protected $mFileProps;
+ /** @var string[] */
protected $mBlackListedExtensions;
- protected $mJavaDetected, $mSVGNSError;
+ /** @var bool|null */
+ protected $mJavaDetected;
+ /** @var string|null */
+ protected $mSVGNSError;
protected static $safeXmlEncodings = [
'UTF-8',
* @todo Replace this with a whitelist filter!
* @param string $element
* @param array $attribs
- * @param array|null $data
+ * @param string|null $data
* @return bool|array
*/
public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
* @return Status[]|bool
*/
public static function getSessionStatus( User $user, $statusKey ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
- $key = $cache->makeKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+ $store = self::getUploadSessionStore();
+ $key = self::getUploadSessionKey( $store, $user, $statusKey );
- return $cache->get( $key );
+ return $store->get( $key );
}
/**
*
* The value will be set in cache for 1 day
*
+ * Avoid triggering this method on HTTP GET/HEAD requests
+ *
* @param User $user
* @param string $statusKey
* @param array|bool $value
* @return void
*/
public static function setSessionStatus( User $user, $statusKey, $value ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
- $key = $cache->makeKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+ $store = self::getUploadSessionStore();
+ $key = self::getUploadSessionKey( $store, $user, $statusKey );
if ( $value === false ) {
- $cache->delete( $key );
+ $store->delete( $key );
} else {
- $cache->set( $key, $value, $cache::TTL_DAY );
+ $store->set( $key, $value, $store::TTL_DAY );
}
}
+
+ /**
+ * @param BagOStuff $store
+ * @param User $user
+ * @param string $statusKey
+ * @return string
+ */
+ private static function getUploadSessionKey( BagOStuff $store, User $user, $statusKey ) {
+ return $store->makeKey(
+ 'uploadstatus',
+ $user->getId() ?: md5( $user->getName() ),
+ $statusKey
+ );
+ }
+
+ /**
+ * @return BagOStuff
+ */
+ private static function getUploadSessionStore() {
+ return ObjectCache::getInstance( 'db-replicated' );
+ }
}
* @author Michael Dale
*/
class UploadFromChunks extends UploadFromFile {
+ /** @var LocalRepo */
+ private $repo;
+ /** @var UploadStash */
+ public $stash;
+ /** @var User */
+ public $user;
+
protected $mOffset;
protected $mChunkIndex;
protected $mFileKey;
protected $mVirtualTempPath;
- /** @var LocalRepo */
- private $repo;
+
+ /** @noinspection PhpMissingParentConstructorInspection */
/**
* Setup local pointers to stash, repo and user (similar to UploadFromStash)
if ( $mode === 'refresh' ) {
$cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
} else {
- $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
+ $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
function () use ( $cache, $key ) {
$cache->delete( $key );
},
$lbFactory = $services->getDBLoadBalancerFactory();
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
+ $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER );
$lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
'〇周后' => '〇周後',
'〇只' => '〇隻',
'〇余' => '〇餘',
+'》里' => '》裡',
'“' => '「',
'”' => '」',
'‘' => '『',
'包准' => '包準',
'包谷' => '包穀',
'包扎' => '包紮',
+'包制' => '包製',
'匏系' => '匏繫',
'北山索面' => '北山索麵',
'北仑河' => '北崙河',
'后安路' => '后安路',
'后平路' => '后平路',
'后庄' => '后庄',
-'后座' => '后座',
'后母戊' => '后母戊',
'后海湾' => '后海灣',
'后海灣' => '后海灣',
'方便面' => '方便麵',
'方向' => '方向',
'方法里' => '方法裡',
+'于吉林' => '於吉林',
'于震中' => '於震中',
'于震前' => '於震前',
'于震后' => '於震後',
'松口镇' => '松口鎮',
'松山庄' => '松山庄',
'松溪县' => '松谿縣',
+'松开始' => '松開始',
'板荡' => '板蕩',
'林宏岳' => '林宏嶽',
'林杰樑' => '林杰樑',
'片里' => '片裡',
'片言只语' => '片言隻語',
'版图里' => '版圖裡',
+'版本里' => '版本裡',
'牙签' => '牙籤',
'牛只' => '牛隻',
'物欲' => '物慾',
'理次发' => '理次髮',
'理发动' => '理發動',
'理发展' => '理發展',
+'理发放' => '理發放',
'理发现' => '理發現',
'理发生' => '理發生',
'理发表' => '理發表',
'空蒙' => '空濛',
'空荡' => '空蕩',
'空荡荡' => '空蕩蕩',
+'空里' => '空裡',
'空钟' => '空鐘',
'空余' => '空餘',
'窒欲' => '窒慾',
'制成' => '製成',
'制毒' => '製毒',
'制法' => '製法',
+'制汉字' => '製漢字',
'制浆' => '製漿',
'制片' => '製片',
'制版' => '製版',
'托交' => '託交',
'托付' => '託付',
'托克逊' => '託克遜',
-'托儿' => '託兒',
'托古讽今' => '託古諷今',
'托名' => '託名',
'托命' => '託命',
'这只比' => '這只比',
'这只用' => '這只用',
'这只能' => '這只能',
+'这只要' => '這只要',
'这只限' => '這只限',
'这只需' => '這只需',
'这只须' => '這只須',
'那只比' => '那只比',
'那只用' => '那只用',
'那只能' => '那只能',
+'那只要' => '那只要',
'那只限' => '那只限',
'那只需' => '那只需',
'那只须' => '那只須',
'厘革' => '釐革',
'金仆姑' => '金僕姑',
'金城里' => '金城里',
+'金发放' => '金發放',
'金范' => '金範',
'金圣叹' => '金聖歎',
'金表情' => '金表情',
'闯荡' => '闖蕩',
'闯炼' => '闖鍊',
'关系' => '關係',
+'关注:' => '關注:',
+'關注:' => '關注:',
'关系列' => '關系列',
'关系所' => '關系所',
'关系科' => '關系科',
'島' => '岛',
'峽' => '峡',
'崍' => '崃',
-'崑' => '昆',
'崗' => '岗',
'崙' => '仑',
'崢' => '峥',
'昇汞' => '升汞',
'陞用' => '升用',
'陞補' => '升补',
+'昇起' => '升起',
'陞遷' => '升迁',
'昇降' => '升降',
'卓著' => '卓著',
'旋乾转坤' => '旋乾转坤',
'無言不讎' => '无言不雠',
'曠若發矇' => '旷若发矇',
+'崑崙' => '昆仑',
+'崑岡' => '昆冈',
+'崑劇' => '昆剧',
+'崑山' => '昆山',
+'崑嵛' => '昆嵛',
+'崑承湖' => '昆承湖',
+'崑曲' => '昆曲',
+'崑腔' => '昆腔',
+'崑蘇' => '昆苏',
+'崑調' => '昆调',
'易·乾' => '易·乾',
'易經·乾' => '易经·乾',
'易经·乾' => '易经·乾',
'瀋液' => '渖液',
'满拚自尽' => '满拚自尽',
'滿拚自盡' => '满拚自尽',
+'靈崑' => '灵昆',
'薰習' => '熏习',
'薰心' => '熏心',
'薰沐' => '熏沐',
'老么' => '老幺',
'肉乾乾' => '肉干干',
'肘手鍊足' => '肘手链足',
+'蘇崑' => '苏昆',
'甦醒' => '苏醒',
'苧烯' => '苧烯',
'薴烯' => '苧烯',
'蔡孝乾' => '蔡孝乾',
'蔡絛' => '蔡絛',
'行餘' => '行馀',
+'西崑' => '西昆',
'覆蓋' => '覆盖',
'見微知著' => '见微知著',
'見著' => '见著',
'局域网' => '區域網',
'局域网络' => '區域網路',
'十杆' => '十桿',
-'特立尼达和托巴哥' => '千里達托貝哥',
-'特立尼達和多巴哥' => '千里達托貝哥',
+'特立尼达和托巴哥' => '千里達及托巴哥',
+'特立尼達和多巴哥' => '千里達及托巴哥',
'不列颠哥伦比亚省' => '卑詩省',
'南朝鲜' => '南韓',
'卡斯特罗' => '卡斯楚',
'塞维利亚' => '塞維亞',
'西維爾' => '塞維亞',
'塞黑' => '塞蒙',
+'塔希提' => '大溪地',
'共和联邦' => '大英國協',
'英联邦' => '大英國協',
'英聯邦' => '大英國協',
'尼日尔' => '尼日',
'尼日爾' => '尼日',
'雅马哈' => '山葉',
-'巴厘岛' => '峇里島',
+'巴厘' => '峇里',
'特朗普' => '川普',
'机床' => '工具機',
'機床' => '工具機',
'萌島' => '曼島',
'马恩岛' => '曼島',
'木杆' => '木桿',
+'尾班車' => '末班車',
'列奥纳多' => '李奧納多',
'杜塞尔多夫' => '杜塞道夫',
'杜塞爾多夫' => '杜塞道夫',
'圣基茨和尼维斯' => '聖克里斯多福及尼維斯',
'聖吉斯納域斯' => '聖克里斯多福及尼維斯',
'聖佐治' => '聖喬治',
+'圣多美和普林西比' => '聖多美普林西比',
+'聖多美和普林西比' => '聖多美普林西比',
'圣文森特和格林纳丁斯' => '聖文森及格瑞那丁',
'聖文森特和格林納丁斯' => '聖文森及格瑞那丁',
'圣赫勒拿' => '聖赫倫那',
'扎伊爾' => '薩伊',
'素檀' => '蘇丹',
'苏里南' => '蘇利南',
+'蘇里南' => '蘇利南',
'浮罗交怡' => '蘭卡威',
'浮羅交怡' => '蘭卡威',
'劳拉' => '蘿拉',
'香煙' => '香菸',
'馬里共和國' => '馬利共和國',
'马里共和国' => '馬利共和國',
+'馬拉維' => '馬拉威',
'马拉维' => '馬拉威',
'馬勒當拿' => '馬拉度納',
'马拉多纳' => '馬拉度納',
'IP地址' => 'IP位址',
'·威尔士' => '·威爾士',
'·威爾士' => '·威爾士',
+'》里' => '》裏',
'一地里' => '一地裏',
'三十六著' => '三十六着',
'三極體' => '三極管',
'夢著述' => '夢著述',
'夢著錄' => '夢著錄',
'梦里' => '夢裏',
+'塔希提' => '大溪地',
'天里' => '天裏',
'宇航员' => '太空人',
'夾著' => '夾着',
'山里的' => '山裏的',
'甘比亞' => '岡比亞',
'岸裡' => '岸裡',
-'巴厘岛' => '峇里島',
+'巴厘' => '峇里',
'工作台' => '工作枱',
'已占' => '已佔',
'巴塞罗那' => '巴塞隆拿',
'爭著錄' => '爭著錄',
'墙里' => '牆裏',
'版图里' => '版圖裏',
+'版本里' => '版本裏',
'版权信息' => '版權資訊',
-'千里達托貝哥' => '特立尼達和多巴哥',
+'千里達及托巴哥' => '特立尼達和多巴哥',
'牽著' => '牽着',
'牽著作' => '牽著作',
'牽著名' => '牽著名',
'空著者' => '空著者',
'空著述' => '空著述',
'空著錄' => '空著錄',
+'空里' => '空裏',
'太空梭' => '穿梭機',
'航天飞机' => '穿梭機',
'穿著' => '穿着',
'聖喬治' => '聖佐治',
'圣基茨和尼维斯' => '聖吉斯納域斯',
'聖克里斯多福及尼維斯' => '聖吉斯納域斯',
+'聖多美普林西比' => '聖多美和普林西比',
'聖文森及格瑞那丁' => '聖文森特和格林納丁斯',
'聖露西亞' => '聖盧西亞',
'聖馬利諾' => '聖馬力諾',
'藏著者' => '藏著者',
'藏著述' => '藏著述',
'藏著錄' => '藏著錄',
+'蘇利南' => '蘇里南',
'蘊涵著' => '蘊涵着',
'蘸著' => '蘸着',
'蘸著作' => '蘸著作',
'馬拉度納' => '馬勒當拿',
'马拉多纳' => '馬勒當拿',
'马拉特·萨芬' => '馬拉特·沙芬',
+'馬拉威' => '馬拉維',
'馬斯垂克' => '馬斯特里赫特',
'馬爾地夫' => '馬爾代夫',
'馬利共和國' => '馬里共和國',
'駕著者' => '駕著者',
'駕著述' => '駕著述',
'駕著錄' => '駕著錄',
+'駛著' => '駛着',
'騎著' => '騎着',
'騎著作' => '騎著作',
'騎著名' => '騎著名',
'騙著者' => '騙著者',
'騙著述' => '騙著述',
'騙著錄' => '騙著錄',
-'驶著' => '驶着',
'体里' => '體裏',
'高畫質' => '高清',
'高著' => '高着',
'全球資訊網' => '万维网',
'三十六著' => '三十六着',
'三極體' => '三极管',
+'上落客' => '上下客',
'下著' => '下着',
'下著作' => '下著作',
'下著名' => '下著名',
'下著稱' => '下著称',
'下著者' => '下著者',
'下著述' => '下著述',
+'落車' => '下车',
'卑詩省' => '不列颠哥伦比亚省',
'不著' => '不着',
'不著書' => '不著书',
'聖露西亞' => '圣卢西亚',
'聖克里斯多福及尼維斯' => '圣基茨和尼维斯',
'聖吉斯納域斯' => '圣基茨和尼维斯',
+'聖多美普林西比' => '圣多美和普林西比',
'聖文森及格瑞那丁' => '圣文森特和格林纳丁斯',
'聖馬利諾' => '圣马力诺',
'蓋亞那' => '圭亚那',
'朝著稱' => '朝著称',
'朝著者' => '朝著者',
'朝著述' => '朝著述',
+'尾班車' => '末班车',
'賓·拉登' => '本·拉登',
'本份' => '本分',
'賓拉登' => '本拉登',
'牽著述' => '牵著述',
'千里達' => '特立尼达',
'千里達及托巴哥' => '特立尼达和多巴哥',
-'千里達托貝哥' => '特立尼达和托巴哥',
'德蕾莎·梅伊' => '特蕾莎·梅',
'文翠珊' => '特蕾莎·梅',
'狗隻' => '犬只',
__METHOD__ );
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $dbDomain = $lbFactory->getLocalDomainID();
+
$n = 0;
foreach ( $result as $row ) {
$pageId = intval( $row->page_id );
[ 'rev_page' => $pageId ],
__METHOD__ );
if ( !$latestTime ) {
- $this->output( wfWikiID() . " $pageId [[$name]] can't find latest rev time?!\n" );
+ $this->output( "$dbDomain $pageId [[$name]] can't find latest rev time?!\n" );
continue;
}
$revision = Revision::loadFromTimestamp( $dbw, $title, $latestTime );
if ( is_null( $revision ) ) {
- $this->output( wfWikiID()
- . " $pageId [[$name]] latest time $latestTime, can't find revision id\n" );
+ $this->output(
+ "$dbDomain $pageId [[$name]] latest time $latestTime, can't find revision id\n"
+ );
continue;
}
$id = $revision->getId();
- $this->output( wfWikiID() . " $pageId [[$name]] latest time $latestTime, rev id $id\n" );
+ $this->output( "$dbDomain $pageId [[$name]] latest time $latestTime, rev id $id\n" );
if ( $this->hasOption( 'fix' ) ) {
$page = WikiPage::factory( $title );
$page->updateRevisionOn( $dbw, $revision );
$this->outputStatus( 'Done!' );
if ( $this->hasOption( 'fix' ) ) {
- $this->outputStatus( ' Cleaned up invalid DB keys on ' . wfWikiID() . "!\n" );
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+ $this->outputStatus( " Cleaned up invalid DB keys on $dbDomain!\n" );
}
}
return;
} elseif ( count( $promotions ) !== 0 ) {
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
$promoText = "User:{$username} into " . implode( ', ', $promotions ) . "...\n";
if ( $exists ) {
- $this->output( wfWikiID() . ": Promoting $promoText" );
+ $this->output( "$dbDomain: Promoting $promoText" );
} else {
- $this->output( wfWikiID() . ": Creating and promoting $promoText" );
+ $this->output( "$dbDomain: Creating and promoting $promoText" );
}
}
$pageRatePart = '-';
$revRatePart = '-';
}
+
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
$this->progress( sprintf(
"%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
. "%d revs (%0.1f|%0.1f/sec all|curr), ETA %s [max %d]",
- $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
+ $now, $dbDomain, $this->ID, $this->pageCount, $pageRate,
$pageRatePart, $this->revCount, $revRate, $revRatePart, $etats,
$this->maxCount
) );
$pageRatePart = '-';
$revRatePart = '-';
}
+
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
$this->progress( sprintf(
"%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), "
. "%d revs (%0.1f|%0.1f/sec all|curr), %0.1f%%|%0.1f%% "
. "prefetched (all|curr), ETA %s [max %d]",
- $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate,
+ $now, $dbDomain, $this->ID, $this->pageCount, $pageRate,
$pageRatePart, $this->revCount, $revRate, $revRatePart,
$fetchRate, $fetchRatePart, $etats, $this->maxCount
) );
$delta = microtime( true ) - $start;
$rate = ( $delta == 0.0 ) ? 0.0 : $migrated / $delta;
$this->output( sprintf( "%s %d (%0.1f%%) done in %0.1f secs (%0.3f accounts/sec).\n",
- wfWikiID(),
+ WikiMap::getCurrentWikiDbDomain()->getId(),
$migrated,
min( $max, $lastUser ) / $lastUser * 100.0,
$delta,
索羅門群島 所罗门群岛
汶萊 文莱
史瓦濟蘭 斯威士兰
+史瓦帝尼 斯威士兰
斯洛維尼亞 斯洛文尼亚
紐西蘭 新西兰
格瑞那達 格林纳达
波士尼亞與赫塞哥維納 波斯尼亚和黑塞哥维那
辛巴威 津巴布韦
宏都拉斯 洪都拉斯
-千里達托貝哥 特立尼达和托巴哥
萬那杜 瓦努阿图
溫納圖 瓦努阿图
葛摩 科摩罗
電視影集 电视系列剧
原子筆 圆珠笔
智慧卡 智能卡
+尾班車 末班车
+落車 下车
+上落客 上下客
鐵達尼號 泰坦尼克号
轉殖 克隆
空中巴士 空中客车
卑詩省 不列颠哥伦比亚省
丹帕沙 登巴萨
峇里 巴厘
-史瓦帝尼 斯威士兰
皮特肯 皮特凯恩
安地卡 安提瓜
撒拉威阿拉伯 阿拉伯撒哈拉
格瑞那丁 格林纳丁斯
普立茲獎 普利策奖
富比士 福布斯
+聖多美普林西比 圣多美和普林西比
公寓里 公寓裏
窝里斗 窩裏鬥
镇里 鎮裏
+》里 》裏
+空里 空裏
+版本里 版本裏
苑裡 苑裡
霄裡 霄裡
岸裡 岸裡
寫著 寫着
遇著 遇着
殺著 殺着
-驶è\91\97 驶着
+é§\9bè\91\97 é§\9b着
著筆 着筆
著鞭 着鞭
著法 着法
厄瓜多尔 厄瓜多爾
厄瓜多爾 厄瓜多爾
厄瓜多 厄瓜多爾
+馬拉威 馬拉維
百慕大 百慕達
厄利垂亞 厄立特里亞
吉布地 吉布堤
索羅門群島 所羅門群島
文莱 汶萊
史瓦濟蘭 斯威士蘭
+史瓦帝尼 斯威士蘭
斯洛維尼亞 斯洛文尼亞
紐西蘭 新西蘭
格瑞那達 格林納達
沙烏地阿拉伯 沙特阿拉伯
辛巴威 津巴布韋
宏都拉斯 洪都拉斯
-千里達托貝哥 特立尼達和多巴哥
+千里達及托巴哥 特立尼達和多巴哥
萬那杜 瓦努阿圖
葛摩 科摩羅
寮國 老撾
北朝鲜 北韓
寮語 老撾語
寮人民民主共和國 老撾人民民主共和國
+蘇利南 蘇里南
莱特湾 雷伊泰灣
萊特灣 雷伊泰灣
蘭卡威 浮羅交怡
帕拉林匹克 殘疾人奧林匹克
不列颠哥伦比亚省 卑詩省
丹帕沙 登巴薩
-巴厘岛 峇里島
-史瓦帝尼 斯威士蘭
+巴厘 峇里
皮特凯恩 皮特肯
安地卡 安提瓜
撒拉威阿拉伯 阿拉伯撒哈拉
疯牛病 瘋牛症
狂牛症 瘋牛症
普利策奖 普立茲獎
+聖多美普林西比 聖多美和普林西比
+塔希提 大溪地
剖釐 剖厘
一釐 一厘
昇平 升平
+昇起 升起
飛昇 飞升
提昇 提升
高昇 高升
滿拚自盡 满拚自尽
拚生尽死 拚生尽死
拚生盡死 拚生尽死
+崑劇 昆剧
+崑山 昆山
+崑岡 昆冈
+崑崙 昆仑
+崑嵛 昆嵛
+崑曲 昆曲
+崑腔 昆腔
+崑蘇 昆苏
+崑調 昆调
+蘇崑 苏昆
+西崑 西昆
+靈崑 灵昆
+崑承湖 昆承湖
所罗门群岛 索羅門群島
所羅門群島 索羅門群島
文莱 汶萊
-斯威士兰 史瓦濟蘭
-斯威士蘭 史瓦濟蘭
+斯威士兰 史瓦帝尼
+斯威士蘭 史瓦帝尼
斯洛文尼亚 斯洛維尼亞
斯洛文尼亞 斯洛維尼亞
新西兰 紐西蘭
津巴布韦 辛巴威
津巴布韋 辛巴威
洪都拉斯 宏都拉斯
-特立尼达和托巴哥 千里達托貝哥
-特立尼達和多巴哥 千里達托貝哥
+特立尼达和托巴哥 千里達及托巴哥
+特立尼達和多巴哥 千里達及托巴哥
瑙鲁 諾魯
瑙魯 諾魯
瓦努阿图 萬那杜
内罗毕 奈洛比
內羅畢 奈洛比
苏里南 蘇利南
+蘇里南 蘇利南
莫桑比克 莫三比克
莱索托 賴索托
萊索托 賴索托
金沙薩 金夏沙
达累斯萨拉姆 三蘭港
马拉维 馬拉威
+馬拉維 馬拉威
留尼汪 留尼旺
布隆方丹 布隆泉
厄瓜多 厄瓜多
东南亚国家联盟 東南亞國家協會
東南亞國家聯盟 東南亞國家協會
哥特式 哥德式
+尾班車 末班車
落車 下車
上落客 上下客
集装箱 貨櫃
不列颠哥伦比亚省 卑詩省
登巴萨 丹帕沙
登巴薩 丹帕沙
-巴厘岛 峇里島
-斯威士兰 史瓦帝尼
-斯威士蘭 史瓦帝尼
+巴厘 峇里
皮特凯恩 皮特肯
安提瓜 安地卡
阿拉伯撒哈拉 撒拉威阿拉伯
格林納丁斯 格瑞那丁
空中客车 空中巴士
普利策奖 普立茲獎
+圣多美和普林西比 聖多美普林西比
+聖多美和普林西比 聖多美普林西比
+塔希提 大溪地
黄岩县 黃巖縣
黄岩区 黃巖區
北仑河 北崙河
+昆剧 崑劇
+昆山 崑山
+昆冈 崑岡
+昆仑 崑崙
昆嵛 崑嵛
-昆承湖 崑承湖
+昆曲 崑曲
+昆腔 崑腔
+昆苏 崑蘇
+昆调 崑調
+苏昆 蘇崑
+西昆 西崑
灵昆 靈崑
+昆承湖 崑承湖
龙岩 龍巖
扑冬 撲鼕
冬冬鼓 鼕鼕鼓
U+05C6D屭|U+05C43屃|
U+05C85岅|U+05742坂|
U+05CDD峝|U+05CD2峒|
-U+05D11崑|U+06606昆|
U+05D19崙|U+04ED1仑|
U+05D57嵗|U+05C81岁|
U+05D7D嵽|U+2BD87𫶇|
這只比
這只限
這只應
+這只要
這只不過
這只包括
那只能
那只比
那只限
那只應
+那只要
那只不過
那只包括
多只能
黑奴籲天錄
林郁方
讚歌
-崑山
-崑曲
-崑腔
-崑調
-崑劇
-崑蘇
-蘇崑
一干家中
星期後
依依不捨
于禁
于敏中
註:# 不作“注:”
+關注:
劃為# 不作“划為”
一個# 避免“個裡”的錯誤
兩個
殿裡
隊裡
詞裡
+》裡
+空裡
+版本裡
裏白 #植物常用名
烏蘇里 #分詞用
夸脫
于丹
于冕
于吉
+於吉林
于堅
于姓
于氏
米瀋
拾瀋
姦污
-託兒
同人誌
文學誌
衝着
燉製
煮製
熬製
+包製
+製漢字 #和製漢字等
遏制 #以下分詞用
管制
抑制
胎發生
結發育
結發表
+金發放
+理發放
古人有云
昔人有云
云敞
哈囉喂
松口鎮
岩松了
+松開始
沙瑯
琺瑯
菜餚
關系統
關系所
關系科
-崑崙
-崑山
-崑劇
-崑曲
-崑腔
-崑蘇
-崑調
-崑岡
-西崑
-蘇崑
銹病
嚐糞
if ( $this->replicaId !== false ) {
$header .= "({$this->replicaId})";
}
- $header .= ' ' . wfWikiID();
+ $header .= ' ' . WikiMap::getCurrentWikiDbDomain()->getId();
LegacyLogger::emit( sprintf( "%-50s %s\n", $header, $msg ), $file );
}
$src = FileBackendGroup::singleton()->get( $this->getOption( 'src' ) );
$posDir = $this->getOption( 'posdir' );
- $posFile = $posDir ? $posDir . '/' . wfWikiID() : false;
+ if ( $posDir != '' ) {
+ $posFile = "$posDir/" . rawurlencode( $src->getDomainId() );
+ } else {
+ $posFile = false;
+ }
if ( $this->hasOption( 'posdump' ) ) {
// Just dump the current position into the specified position dir
require_once __DIR__ . '/Maintenance.php';
use Wikimedia\Rdbms\IMaintainableDatabase;
+use Wikimedia\Rdbms\DatabaseSqlite;
/**
* Maintenance script to run database schema updates.
$this->fatalError( $text );
}
- $this->output( "Going to run database updates for " . wfWikiID() . "\n" );
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+ $this->output( "Going to run database updates for $dbDomain\n" );
if ( $db->getType() === 'sqlite' ) {
/** @var IMaintainableDatabase|DatabaseSqlite $db */
$this->output( "Using SQLite file: '{$db->getDbFilePath()}'\n" );
}
public function execute() {
- $posFile = $this->getOption( 'p', 'searchUpdate.' . wfWikiID() . '.pos' );
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+ $posFile = $this->getOption( 'p', 'searchUpdate.' . rawurlencode( $dbDomain ) . '.pos' );
$end = $this->getOption( 'e', wfTimestampNow() );
if ( $this->hasOption( 's' ) ) {
$start = $this->getOption( 's' );
font-weight: bold;
}
-/* success and error messages */
+/* Error, warning and success messages */
.error,
.warning,
.success {
}
.warning {
- color: #705000;
+ color: #ac6600;
}
.success {
- color: #009000;
+ color: #14866d;
}
.errorbox,
padding: 0.5em 1em;
margin-bottom: 1em;
display: inline-block;
- zoom: 1;
- *display: inline; /* stylelint-disable-line declaration-block-no-duplicate-properties */
}
.errorbox h2,
.warningbox h2,
.successbox h2 {
- font-size: 1em;
color: inherit;
+ font-size: 1em;
font-weight: bold;
display: inline;
margin: 0 0.5em 0 0;
}
.errorbox {
- color: #d33;
- border-color: #fac5c5;
- background-color: #fae3e3;
+ background-color: #fee7e6;
+ color: #000;
+ border-color: #d33;
}
.warningbox {
- color: #705000;
- border-color: #fde29b;
- background-color: #fdf1d1;
+ background-color: #fef6e7;
+ color: #000;
+ border-color: #fc3;
}
.successbox {
- color: #008000;
- border-color: #b7fdb5;
- background-color: #e1fddf;
+ background-color: #d5fdf4;
+ color: #000;
+ border-color: #14866d;
}
/* general info/warning box for SP */
.mw-infobox {
- border: 2px solid #ff7f00;
+ border: 2px solid #fc3;
margin: 0.5em;
clear: left;
overflow: hidden;
// Text colors
@colorText: @colorGray2;
+@colorTextEmphasized: @colorGray1;
@colorTextLight: @colorGray5;
@colorButtonText: @colorGray2;
@colorButtonTextHighlight: @colorGray4;
// Form input sizes, equal to OOUI at 14px base font-size
@sizeInputBinary: 1.5625em;
+
+// Messages
+@backgroundColorError: #fee7e6;
+@borderColorError: #d33;
+@backgroundColorWarning: #fef6e7;
+@borderColorWarning: #fc3;
.box-sizing( border-box );
font-size: 0.9em;
margin: 0 0 1em 0;
- padding: 0.5em;
+ padding: 0.5em 1em;
word-wrap: break-word;
}
// Colours taken from those for .errorbox in shared.css
.error {
- color: @colorErrorText;
- border: 1px solid #fac5c5;
- background-color: #fae3e3;
+ background-color: @backgroundColorError;
+ color: @colorTextEmphasized;
+ border: 1px solid @borderColorError;
}
// Colours taken from those for .warningbox in shared.css
.warning {
- color: @colorWarningText;
- border: 1px solid #fde29b;
- background-color: #fdf1d1;
+ background-color: @backgroundColorWarning;
+ color: @colorTextEmphasized;
+ border: 1px solid @borderColorWarning;
}
// This specifies styling for individual field validation error messages.
public static function applyInitialConfig() {
global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgMainWANCache;
global $wgMainStash;
+ global $wgObjectCaches;
global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
global $wgLocaltimezone, $wgLocalisationCacheConf;
global $wgSearchType;
$wgLanguageConverterCacheType = 'hash';
// Uses db-replicated in DefaultSettings
$wgMainStash = 'hash';
+ // Use hash instead of db
+ $wgObjectCaches['db-replicated'] = $wgObjectCaches['hash'];
// Use memory job queue
$wgJobTypeConf = [
'default' => [ 'class' => JobQueueMemory::class, 'order' => 'fifo' ],
// Wipe WANObjectCache process cache, which is invalidated by article insertion
// due to T144706
- ObjectCache::getMainWANInstance()->clearProcessCache();
+ MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
$this->executeSetupSnippets( $teardown );
}
/**
* @dataProvider provideDomainCheck
- * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseWikiId
+ * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseDomain
*/
public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
$this->setMwGlobals(
<?php
+use MediaWiki\Interwiki\InterwikiLookup;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
];
}
+ public function provideSubpage() {
+ // NOTE: avoid constructing Title objects in the provider, since it may access the database.
+ return [
+ [ 'Foo', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ],
+ [ 'Foo#bar', 'x', new TitleValue( NS_MAIN, 'Foo/x' ) ],
+ [ 'User:Foo', 'x', new TitleValue( NS_USER, 'Foo/x' ) ],
+ [ 'wiki:User:Foo', 'x', new TitleValue( NS_MAIN, 'User:Foo/x', '', 'wiki' ) ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSubpage
+ * @covers Title::getSubpage
+ */
+ public function testSubpage( $title, $sub, LinkTarget $expected ) {
+ $interwikiLookup = $this->getMock( InterwikiLookup::class );
+ $interwikiLookup->expects( $this->any() )
+ ->method( 'isValidInterwiki' )
+ ->willReturnCallback(
+ function ( $prefix ) {
+ return $prefix == 'wiki';
+ }
+ );
+
+ $this->setService( 'InterwikiLookup', $interwikiLookup );
+
+ $title = Title::newFromText( $title );
+ $expected = Title::newFromLinkTarget( $expected );
+ $actual = $title->getSubpage( $sub );
+
+ // NOTE: convert to string for comparison
+ $this->assertSame( $expected->getPrefixedText(), $actual->getPrefixedText(), 'text form' );
+ $this->assertTrue( $expected->equals( $actual ), 'Title equality' );
+ }
+
public static function provideNewFromTitleValue() {
return [
[ new TitleValue( NS_MAIN, 'Foo' ) ],
$this->assertEquals( $wiki, WikiMap::getWikiFromUrl( $url ) );
}
- public function provideGetWikiIdFromDomain() {
+ public function provideGetWikiIdFromDbDomain() {
return [
[ 'db-prefix_', 'db-prefix_' ],
[ wfWikiID(), wfWikiID() ],
}
/**
- * @dataProvider provideGetWikiIdFromDomain
+ * @dataProvider provideGetWikiIdFromDbDomain
* @covers WikiMap::getWikiIdFromDbDomain()
*/
- public function testGetWikiIdFromDomain( $domain, $wikiId ) {
+ public function testGetWikiIdFromDbDomain( $domain, $wikiId ) {
$this->assertEquals( $wikiId, WikiMap::getWikiIdFromDbDomain( $domain ) );
}
new TitleValue( 1, 'ApiQueryWatchlistRawIntegrationTestPage1' ),
] );
- ObjectCache::getMainWANInstance()->clearProcessCache();
+ MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
$result = $this->doListWatchlistRawRequest( [
'wrowner' => $otherUser->getName(),
'wrtoken' => '1234567890',
}
) );
$lb1->method( 'getMasterPos' )->willReturn( $m1Pos );
+ $lb1->method( 'getReplicaResumePos' )->willReturn( $m1Pos );
$lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
// Master DB 2
$mockDB2 = $this->getMockBuilder( IDatabase::class )
}
) );
$lb2->method( 'getMasterPos' )->willReturn( $m2Pos );
+ $lb2->method( 'getReplicaResumePos' )->willReturn( $m2Pos );
$lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
$bag = new HashBagOStuff();
}
/**
- * @covers LoadBalancer::getLocalDomainID()
- * @covers LoadBalancer::resolveDomainID()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getLocalDomainID()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::resolveDomainID()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
*/
public function testWithoutReplica() {
global $wgDBname;
}
] );
+ $this->assertEquals( 1, $lb->getServerCount() );
+ $this->assertFalse( $lb->hasReplicaServers() );
+ $this->assertFalse( $lb->hasStreamingReplicaServers() );
+
+ $this->assertTrue( $lb->haveIndex( 0 ) );
+ $this->assertFalse( $lb->haveIndex( 1 ) );
+ $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
+ $this->assertFalse( $lb->isNonZeroLoad( 1 ) );
+
$ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
$this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
$this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
$lb->closeAll();
}
+ /**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getReaderIndex()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getServerName()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getServerInfo()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getServerType()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getServerAttributes()
+ */
public function testWithReplica() {
global $wgDBserver;
$this->assertTrue( $lb->hasReplicaServers() );
$this->assertTrue( $lb->hasStreamingReplicaServers() );
+ $this->assertTrue( $lb->haveIndex( 0 ) );
+ $this->assertTrue( $lb->haveIndex( 1 ) );
+ $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
+ $this->assertTrue( $lb->isNonZeroLoad( 1 ) );
+
+ for ( $i = 0; $i < $lb->getServerCount(); ++$i ) {
+ $this->assertType( 'string', $lb->getServerName( $i ) );
+ $this->assertType( 'array', $lb->getServerInfo( $i ) );
+ $this->assertType( 'string', $lb->getServerType( $i ) );
+ $this->assertType( 'array', $lb->getServerAttributes( $i ) );
+ }
+
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
'cluster master set' );
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
$this->assertWriteForbidden( $dbr );
+ $this->assertEquals( $dbr->getLBInfo( 'serverIndex' ), $lb->getReaderIndex() );
if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
$dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
}
/**
- * @covers LoadBalancer::openConnection()
- * @covers LoadBalancer::getAnyOpenConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getAnyOpenConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
*/
function testOpenConnection() {
$lb = $this->newSingleServerLocalLoadBalancer();
$lb->closeAll();
}
+ /**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::forEachOpenMasterConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::setTransactionListener()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::beginMasterChanges()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::finalizeMasterChanges()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::approveMasterChanges()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::commitMasterChanges()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionIdleCallbacks()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionListenerCallbacks()
+ */
public function testTransactionCallbackChains() {
global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
$conn2->close();
}
+ /**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+ */
public function testDBConnRefReadsMasterAndReplicaRoles() {
$lb = $this->newSingleServerLocalLoadBalancer();
}
/**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRole() {
}
/**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRoleIndex() {
}
/**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRoleInsert() {
$rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
}
+ /**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getMaintenanceConnectionRef()
+ */
public function testQueryGroupIndex() {
$lb = $this->newMultiServerLocalLoadBalancer( [ 'defaultGroup' => false ] );
/** @var LoadBalancer $lbWrapper */
$rRC = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
$rWL = $lb->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
+ $rRCMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
+ $rWLMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'watchlist' ] );
$this->assertEquals( 3, $rRC->getLBInfo( 'serverIndex' ) );
$this->assertEquals( 3, $rWL->getLBInfo( 'serverIndex' ) );
+ $this->assertEquals( 3, $rRCMaint->getLBInfo( 'serverIndex' ) );
+ $this->assertEquals( 3, $rWLMaint->getLBInfo( 'serverIndex' ) );
$rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
$logIndexPicked = $rLog->getLBInfo( 'serverIndex' );
$rGeneric = $lb->getConnectionRef( DB_REPLICA );
$this->assertEquals( $lb->getWriterIndex(), $rGeneric->getLBInfo( 'serverIndex' ) );
}
+
+ /**
+ * @covers \Wikimedia\Rdbms\LoadBalancer::getLazyConnectionRef
+ */
+ public function testGetLazyConnectionRef() {
+ $lb = $this->newMultiServerLocalLoadBalancer();
+
+ $rMaster = $lb->getLazyConnectionRef( DB_MASTER );
+ $rReplica = $lb->getLazyConnectionRef( 1 );
+ $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
+ $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
+
+ $rMaster->getType();
+ $rReplica->getType();
+ $rMaster->getDomainID();
+ $rReplica->getDomainID();
+ $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
+ $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
+
+ $rMaster->query( "SELECT 1", __METHOD__ );
+ $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
+
+ $rReplica->query( "SELECT 1", __METHOD__ );
+ $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
+ $this->assertNotFalse( $lb->getAnyOpenConnection( 1 ) );
+ }
}
$fi = SiteStats::images();
$ai = SiteStats::articles();
+ $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
+
$dbw->begin( __METHOD__ ); // block opportunistic updates
- $update = SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] );
- $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() );
- $update->doUpdate();
+ DeferredUpdates::addUpdate(
+ SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] )
+ );
$this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() );
// Still the same
--- /dev/null
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class HTTPFileStreamerTest extends TestCase {
+
+ /**
+ * @covers HTTPFileStreamer::preprocessHeaders
+ * @dataProvider providePreprocessHeaders
+ */
+ public function testPreprocessHeaders( array $input, array $expectedRaw, array $expectedOpt ) {
+ list( $actualRaw, $actualOpt ) = HTTPFileStreamer::preprocessHeaders( $input );
+ $this->assertSame( $expectedRaw, $actualRaw );
+ $this->assertSame( $expectedOpt, $actualOpt );
+ }
+
+ public function providePreprocessHeaders() {
+ return [
+ [
+ [ 'Vary' => 'cookie', 'Cache-Control' => 'private' ],
+ [ 'Vary: cookie', 'Cache-Control: private' ],
+ [],
+ ],
+ [
+ [
+ 'Range' => 'bytes=(123-456)',
+ 'Content-Type' => 'video/mp4',
+ 'If-Modified-Since' => 'Wed, 21 Oct 2015 07:28:00 GMT',
+ ],
+ [ 'Content-Type: video/mp4' ],
+ [ 'range' => 'bytes=(123-456)', 'if-modified-since' => 'Wed, 21 Oct 2015 07:28:00 GMT' ],
+ ],
+ ];
+ }
+
+}
* @param int $ttl
*/
public function testSetAndGet( $value, $ttl ) {
+ $cache = $this->cache;
+
$curTTL = null;
$asOf = null;
- $key = $this->cache->makeKey( 'x', wfRandomString() );
+ $key = $cache->makeKey( 'x', wfRandomString() );
- $this->cache->get( $key, $curTTL, [], $asOf );
+ $cache->get( $key, $curTTL, [], $asOf );
$this->assertNull( $curTTL, "Current TTL is null" );
$this->assertNull( $asOf, "Current as-of-time is infinite" );
$t = microtime( true );
- $this->cache->set( $key, $value, $ttl );
- $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
+ $cache->set( $key, $value, $cache::TTL_UNCACHEABLE );
+ $cache->get( $key, $curTTL, [], $asOf );
+ $this->assertNull( $curTTL, "Current TTL is null (TTL_UNCACHEABLE)" );
+ $this->assertNull( $asOf, "Current as-of-time is infinite (TTL_UNCACHEABLE)" );
+
+ $cache->set( $key, $value, $ttl );
+
+ $this->assertEquals( $value, $cache->get( $key, $curTTL, [], $asOf ) );
if ( is_infinite( $ttl ) || $ttl == 0 ) {
$this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
} else {
$this->assertEquals( 6, $hit, "New values cached" );
foreach ( $keys as $i => $key ) {
- // Should evict from process cache
+ // Should not evict from process cache
$this->cache->delete( $key );
$mockWallClock += 0.001; // cached values will be newer than tombstone
// Get into cache (specific process cache group)
/**
* @dataProvider getWithSetCallback_provider
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
* @param array $extOpts
* @param bool $versioned
*/
$curTTL = null;
$v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
- if ( $versioned ) {
- $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
- } else {
- $this->assertEquals( $value, $v, "Value returned" );
- }
+ $this->assertEquals( $value, $v, "Value returned" );
$this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
$wasSet = 0;
/**
* @dataProvider getWithSetCallback_provider
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
* @param array $extOpts
* @param bool $versioned
*/
$this->assertEquals( 2, $wasSet, "Value re-calculated" );
}
- /**
- * @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
- */
- public function testGetWithSetCallback_invalidCallback() {
- $this->setExpectedException( InvalidArgumentException::class );
- $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' );
- }
-
/**
* @dataProvider getMultiWithSetCallback_provider
* @covers WANObjectCache::getMultiWithSetCallback
$value = "@efef$";
$keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
$v = $cache->getMultiWithSetCallback(
- $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+ $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
$this->assertEquals( $value, $v[$keyB], "Value returned" );
$this->assertEquals( 1, $wasSet, "Value regenerated" );
- $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+ $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
+
$v = $cache->getMultiWithSetCallback(
- $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+ $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
$this->assertEquals( $value, $v[$keyB], "Value returned" );
$this->assertEquals( 1, $wasSet, "Value not regenerated" );
- $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+ $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
$mockWallClock += 1;
$curTTL = null;
$v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
- if ( $versioned ) {
- $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
- } else {
- $this->assertEquals( $value, $v, "Value returned" );
- }
+ $this->assertEquals( $value, $v, "Value returned" );
$this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
$wasSet = 0;
$keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
$this->assertEquals( $value, $v[$keyB], "Value returned" );
$this->assertEquals( 1, $wasSet, "Value regenerated" );
- $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+ $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
+
$v = $cache->getMultiWithUnionSetCallback(
$keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
$this->assertEquals( $value, $v[$keyB], "Value returned" );
$this->assertEquals( 1, $wasSet, "Value not regenerated" );
- $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+ $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
$mockWallClock += 1;
$curTTL = null;
$v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
- if ( $versioned ) {
- $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
- } else {
- $this->assertEquals( $value, $v, "Value returned" );
- }
+ $this->assertEquals( $value, $v, "Value returned" );
$this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
$wasSet = 0;
/**
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
*/
public function testLockTSE() {
$cache = $this->cache;
/**
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
* @covers WANObjectCache::set()
*/
public function testLockTSESlow() {
/**
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
*/
public function testBusyValue() {
$cache = $this->cache;
/**
* @dataProvider getWithSetCallback_versions_provider
* @covers WANObjectCache::getWithSetCallback()
- * @covers WANObjectCache::doGetWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
* @param array $extOpts
* @param bool $versioned
*/
$this->internalCache->set(
WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
[
- WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+ WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
WANObjectCache::FLD_VALUE => $value,
WANObjectCache::FLD_TTL => 3600,
WANObjectCache::FLD_TIME => $goodTime
$this->internalCache->set(
WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
[
- WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+ WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
WANObjectCache::FLD_VALUE => $value,
WANObjectCache::FLD_TTL => 3600,
WANObjectCache::FLD_TIME => $badTime
->setMethods( [ 'get', 'changeTTL' ] )->getMock();
$backend->expects( $this->once() )->method( 'get' )
->willReturn( [
- WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+ WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
WANObjectCache::FLD_VALUE => 'value',
WANObjectCache::FLD_TTL => 3600,
WANObjectCache::FLD_TIME => 300,
}
public function testGetLinkClasses() {
- $wanCache = ObjectCache::getMainWANInstance();
- $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
- $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ $services = MediaWikiServices::getInstance();
+ $wanCache = $services->getMainWANObjectCache();
+ $titleFormatter = $services->getTitleFormatter();
+ $nsInfo = $services->getNamespaceInfo();
$linkCache = new LinkCache( $titleFormatter, $wanCache, $nsInfo );
$foobarTitle = new TitleValue( NS_MAIN, 'FooBar' );
$redirectTitle = new TitleValue( NS_MAIN, 'Redirect' );
$this->db->delete( 'actor', [ 'actor_user' => $id ], __METHOD__ );
User::purge( $domain, $id );
// Because WANObjectCache->delete() stupidly doesn't delete from the process cache.
- ObjectCache::getMainWANInstance()->clearProcessCache();
+
+ MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache();
$user = User::newFromId( $id );
$this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' );