* $wgEnableSpecialMute (T218265) - This configuration controls whether
Special:Mute is available and whether to include a link to it on emails
originating from Special:Email.
+* editmyuserjsredirect user right – users without this right now cannot edit JS
+ redirects in their userspace unless the target of the redirect is also in
+ their userspace. By default, this right is given to everyone.
==== Changed configuration ====
* $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four
of headers in private wikis.
* Language::formatTimePeriod now supports the new 'avoidhours' option to output
strings like "5 days ago" instead of "5 days 13 hours ago".
+* (T220163) Added SpecialMuteModifyFormFields hook to allow extensions
+ to add fields to Special:Mute.
=== External library changes in 1.34 ===
deprecated since 1.33.
* The static properties mw.Api.errors and mw.Api.warnings, deprecated in 1.29,
have been removed.
+* The UploadVerification hook, deprecated in 1.28, has been removed. Instead,
+ use the UploadVerifyFile hook.
+* UploadBase:: and UploadFromChunks::stashFileGetKey() and stashSession(),
+ deprecated in 1.28, have been removed. Instead, please use the getFileKey()
+ method on the response from doStashFile().
* …
=== Deprecations in 1.34 ===
'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php',
'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
+ 'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
&$oldTitle: old title (object)
&$newTitle: new title (object)
+'SpecialMuteModifyFormFields': Add more fields to Special:Mute
+$sp: SpecialPage object, for context
+&$fields: Current HTMLForm fields descriptors
+
'SpecialNewpagesConditions': Called when building sql query for
Special:NewPages.
&$special: NewPagesPager object (subclass of ReverseChronologicalPager)
MessageSpecifier instance (you might want to use ApiMessage to provide machine
-readable details for the API).
-'UploadVerification': DEPRECATED since 1.28! Use UploadVerifyFile instead.
-Additional chances to reject an uploaded file.
-$saveName: (string) destination file name
-$tempName: (string) filesystem path to the temporary file for checks
-&$error: (string) output: message key for message to show if upload canceled by
- returning false. May also be an array, where the first element is the message
- key and the remaining elements are used as parameters to the message.
-
'UploadVerifyFile': extra file verification, based on MIME type, etc. Preferred
in most cases over UploadVerification.
$upload: (object) an instance of UploadBase, with all info about the upload
$wgGroupPermissions['user']['editmyusercss'] = true;
$wgGroupPermissions['user']['editmyuserjson'] = true;
$wgGroupPermissions['user']['editmyuserjs'] = true;
+$wgGroupPermissions['user']['editmyuserjsredirect'] = true;
$wgGroupPermissions['user']['purge'] = true;
$wgGroupPermissions['user']['sendemail'] = true;
$wgGroupPermissions['user']['applychangetags'] = true;
// Disable rate-limiting to allow integration tests to run unthrottled
// in CI and for devs locally (T225796)
$wgRateLimits = [];
+
+// Disable legacy javascript globals in CI and for devs (T72470)
+$wgLegacyJavaScriptGlobals = true;
* @return string
*/
function wfGlobalCacheKey( ...$args ) {
+ wfDeprecated( __METHOD__, '1.30' );
return ObjectCache::getLocalClusterInstance()->makeGlobalKey( ...$args );
}
use Exception;
use Hooks;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Session\SessionManager;
use MediaWiki\Special\SpecialPageFactory;
use MediaWiki\User\UserIdentity;
/** @var SpecialPageFactory */
private $specialPageFactory;
+ /** @var RevisionLookup */
+ private $revisionLookup;
+
/** @var string[] List of pages names anonymous user may see */
private $whitelistRead;
'editmyusercss',
'editmyuserjson',
'editmyuserjs',
+ 'editmyuserjsredirect',
'editmywatchlist',
'editsemiprotected',
'editsitecss',
/**
* @param SpecialPageFactory $specialPageFactory
+ * @param RevisionLookup $revisionLookup
* @param string[] $whitelistRead
* @param string[] $whitelistReadRegexp
* @param bool $emailConfirmToEdit
*/
public function __construct(
SpecialPageFactory $specialPageFactory,
+ RevisionLookup $revisionLookup,
$whitelistRead,
$whitelistReadRegexp,
$emailConfirmToEdit,
NamespaceInfo $nsInfo
) {
$this->specialPageFactory = $specialPageFactory;
+ $this->revisionLookup = $revisionLookup;
$this->whitelistRead = $whitelistRead;
$this->whitelistReadRegexp = $whitelistReadRegexp;
$this->emailConfirmToEdit = $emailConfirmToEdit;
&& !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
) {
$errors[] = [ 'mycustomjsprotected', $action ];
+ } elseif (
+ $page->isUserJsConfigPage()
+ && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
+ ) {
+ // T207750 - do not allow users to edit a redirect if they couldn't edit the target
+ $rev = $this->revisionLookup->getRevisionByTitle( $page );
+ $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
+ $target = $content ? $content->getUltimateRedirectTarget() : null;
+ if ( $target && (
+ !$target->inNamespace( NS_USER )
+ || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
+ ) ) {
+ $errors[] = [ 'mycustomjsredirectprotected', $action ];
+ }
}
} else {
// Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
$config = $services->getMainConfig();
return new PermissionManager(
$services->getSpecialPageFactory(),
+ $services->getRevisionLookup(),
$config->get( 'WhitelistRead' ),
$config->get( 'WhitelistReadRegexp' ),
$config->get( 'EmailConfirmToEdit' ),
}
/**
- * @param bool|IResultWrapper $row
+ * @param bool|stdClass $row
* @return void
*/
protected function setCurrent( $row ) {
// XXX Is it ok to put untrusted data into log??
'csp-report' => $report,
'method' => __METHOD__,
- 'user_id' => $this->getUser()->getId() || 'logged-out',
+ 'user_id' => $this->getUser()->getId() ?: 'logged-out',
'user-agent' => $userAgent,
'source' => $this->getParameter( 'source' ),
] );
__METHOD__ . "($code)-big"
);
foreach ( $res as $row ) {
- $name = $this->contLang->lcfirst( $row->page_title );
// Include entries/stubs for all keys in $mostused in adaptive mode
- if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $name, $overridable ) ) {
+ if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $row->page_title, $overridable ) ) {
$cache[$row->page_title] = '!TOO BIG';
}
// At least include revision ID so page changes are reflected in the hash
$revQuery['joins']
);
foreach ( $res as $row ) {
- $name = $this->contLang->lcfirst( $row->page_title );
// Include entries/stubs for all keys in $mostused in adaptive mode
- if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $name, $overridable ) ) {
+ if ( $wgAdaptiveMessageCache || $this->isMainCacheable( $row->page_title, $overridable ) ) {
try {
$rev = $revisionStore->newRevisionFromRow( $row );
$content = $rev->getContent( MediaWiki\Revision\SlotRecord::MAIN );
}
/**
- * @param string $name Message name with lowercase first letter
+ * @param string $name Message name (possibly with /code suffix)
* @param array $overridable Map of (key => unused) for software-defined messages
* @return bool
*/
private function isMainCacheable( $name, array $overridable ) {
+ // Convert first letter to lowercase, and strip /code suffix
+ $name = $this->contLang->lcfirst( $name );
+ $msg = preg_replace( '/\/[a-z0-9-]{2,}$/', '', $name );
// Include common conversion table pages. This also avoids problems with
// Installer::parse() bailing out due to disallowed DB queries (T207979).
- return ( isset( $overridable[$name] ) || strpos( $name, 'conversiontable/' ) === 0 );
+ return ( isset( $overridable[$msg] ) || strpos( $name, 'conversiontable/' ) === 0 );
}
/**
);
} else {
// Message page either does not exist or does not override a software message
- $name = $this->contLang->lcfirst( $title );
- if ( !$this->isMainCacheable( $name, $this->overridable ) ) {
+ if ( !$this->isMainCacheable( $title, $this->overridable ) ) {
// Message page does not override any software-defined message. A custom
// message might be defined to have content or settings specific to the wiki.
// Load the message page, utilizing the individual message cache as needed.
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\DBTransactionError;
/**
* Class for managing the deferred updates
* @since 1.34
*/
public static function attemptUpdate( DeferrableUpdate $update, ILBFactory $lbFactory ) {
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ if ( !$ticket || $lbFactory->hasTransactionRound() ) {
+ throw new DBTransactionError( null, "A database transaction round is pending." );
+ }
+
if ( $update instanceof DataUpdate ) {
- $update->setTransactionTicket( $lbFactory->getEmptyTransactionTicket( __METHOD__ ) );
+ $update->setTransactionTicket( $ticket );
}
- if (
+ $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ $useExplicitTrxRound = !(
$update instanceof TransactionRoundAwareUpdate &&
$update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
- ) {
- $fnameTrxOwner = null;
+ );
+ // Flush any pending changes left over from an implicit transaction round
+ if ( $useExplicitTrxRound ) {
+ $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round
} else {
- $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ $lbFactory->commitMasterChanges( $fnameTrxOwner ); // new implicit round
}
-
- if ( $fnameTrxOwner !== null ) {
- $lbFactory->beginMasterChanges( $fnameTrxOwner );
- }
-
+ // Run the update after any stale master view snapshots have been flushed
$update->doUpdate();
-
- if ( $fnameTrxOwner !== null ) {
- $lbFactory->commitMasterChanges( $fnameTrxOwner );
- }
+ // Commit any pending changes from the explicit or implicit transaction round
+ $lbFactory->commitMasterChanges( $fnameTrxOwner );
}
/**
}
public function validate( $value, $alldata ) {
- if ( !$this->mParams['exists'] ) {
+ if ( !$this->mParams['exists'] || $value === '' ) {
return true;
}
/**
* @deprecated DO NOT CALL ME.
- * This method was introduced when factoring UploadImporter out of WikiRevision.
- * It only has 1 use by the deprecated downloadSource method in WikiRevision.
- * Do not use this in new code.
+ * This method was introduced when factoring (Importable)UploadRevisionImporter out of
+ * WikiRevision. It only has 1 use by the deprecated downloadSource method in WikiRevision.
+ * Do not use this in new code, it will be made private soon.
*
* @param ImportableUploadRevision $wikiRevision
*
/**
* @since 1.12.2
- * @deprecated in 1.31. Use UploadImporter::import
+ * @deprecated in 1.31. Use UploadRevisionImporter::import
* @return bool
*/
public function importUpload() {
/**
* @since 1.12.2
- * @deprecated in 1.31. Use UploadImporter::downloadSource
+ * @deprecated in 1.31. No replacement
* @return bool|string
*/
public function downloadSource() {
public static function overrideConfig() {
// Use PHP's built-in session handling, since MediaWiki's
// SessionHandler can't work before we have an object cache set up.
- define( 'MW_NO_SESSION_HANDLER', 1 );
+ if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
+ define( 'MW_NO_SESSION_HANDLER', 1 );
+ }
// Don't access the database
$GLOBALS['wgUseDatabaseMessages'] = false;
*
* @ingroup Cache
*/
-class APCBagOStuff extends BagOStuff {
+class APCBagOStuff extends MediumSpecificBagOStuff {
/** @var bool Whether to trust the APC implementation to serialization */
private $nativeSerialize;
*
* @ingroup Cache
*/
-class APCUBagOStuff extends BagOStuff {
+class APCUBagOStuff extends MediumSpecificBagOStuff {
/** @var bool Whether to trust the APC implementation to serialization */
private $nativeSerialize;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
-use Wikimedia\WaitConditionLoop;
/**
* Class representing a cache/ephemeral data store
* @ingroup Cache
*/
abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInterface {
- /** @var array[] Lock tracking */
- protected $locks = [];
- /** @var int ERR_* class constant */
- protected $lastError = self::ERR_NONE;
- /** @var string */
- protected $keyspace = 'local';
/** @var LoggerInterface */
protected $logger;
+
/** @var callable|null */
protected $asyncHandler;
- /** @var int Seconds */
- protected $syncTimeout;
- /** @var int Bytes; chunk size of segmented cache values */
- protected $segmentationSize;
- /** @var int Bytes; maximum total size of a segmented cache value */
- protected $segmentedValueMaxSize;
+ /** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */
+ protected $attrMap = [];
/** @var bool */
- private $debugMode = false;
- /** @var array */
- private $duplicateKeyLookups = [];
- /** @var bool */
- private $reportDupes = false;
- /** @var bool */
- private $dupeTrackScheduled = false;
-
- /** @var callable[] */
- protected $busyCallbacks = [];
+ protected $debugMode = false;
/** @var float|null */
private $wallClockOverride;
- /** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */
- protected $attrMap = [];
-
- /** Bitfield constants for get()/getMulti() */
- const READ_LATEST = 1; // use latest data for replicated stores
- const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
- /** Bitfield constants for set()/merge() */
- const WRITE_SYNC = 4; // synchronously write to all locations for replicated stores
- const WRITE_CACHE_ONLY = 8; // Only change state of the in-memory cache
- const WRITE_ALLOW_SEGMENTS = 16; // Allow partitioning of the value if it is large
- const WRITE_PRUNE_SEGMENTS = 32; // Delete all partition segments of the value
-
- /** @var string Component to use for key construction of blob segment keys */
- const SEGMENT_COMPONENT = 'segment';
+ /** Bitfield constants for get()/getMulti(); these are only advisory */
+ const READ_LATEST = 1; // if supported, avoid reading stale data due to replication
+ const READ_VERIFIED = 2; // promise that the caller handles detection of staleness
+ /** Bitfield constants for set()/merge(); these are only advisory */
+ const WRITE_SYNC = 4; // if supported, block until the write is fully replicated
+ const WRITE_CACHE_ONLY = 8; // only change state of the in-memory cache
+ const WRITE_ALLOW_SEGMENTS = 16; // allow partitioning of the value if it is large
+ const WRITE_PRUNE_SEGMENTS = 32; // delete all the segments if the value is partitioned
+ const WRITE_BACKGROUND = 64; // if supported, do not block on completion until the next read
/**
- * $params include:
+ * Parameters include:
* - logger: Psr\Log\LoggerInterface instance
- * - keyspace: Default keyspace for $this->makeKey()
* - asyncHandler: Callable to use for scheduling tasks after the web request ends.
* In CLI mode, it should run the task immediately.
- * - reportDupes: Whether to emit warning log messages for all keys that were
- * requested more than once (requires an asyncHandler).
- * - syncTimeout: How long to wait with WRITE_SYNC in seconds.
- * - segmentationSize: The chunk size, in bytes, of segmented values. The value should
- * not exceed the maximum size of values in the storage backend, as configured by
- * the site administrator.
- * - segmentedValueMaxSize: The maximum total size, in bytes, of segmented values.
- * This should be configured to a reasonable size give the site traffic and the
- * amount of I/O between application and cache servers that the network can handle.
* @param array $params
*/
public function __construct( array $params = [] ) {
$this->setLogger( $params['logger'] ?? new NullLogger() );
-
- if ( isset( $params['keyspace'] ) ) {
- $this->keyspace = $params['keyspace'];
- }
-
$this->asyncHandler = $params['asyncHandler'] ?? null;
-
- if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
- $this->reportDupes = true;
- }
-
- $this->syncTimeout = $params['syncTimeout'] ?? 3;
- $this->segmentationSize = $params['segmentationSize'] ?? 8388608; // 8MiB
- $this->segmentedValueMaxSize = $params['segmentedValueMaxSize'] ?? 67108864; // 64MiB
}
/**
}
/**
- * @param bool $bool
+ * @param bool $enabled
*/
- public function setDebug( $bool ) {
- $this->debugMode = $bool;
+ public function setDebug( $enabled ) {
+ $this->debugMode = $enabled;
}
/**
* @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
* @return mixed Returns false on failure or if the item does not exist
*/
- public function get( $key, $flags = 0 ) {
- $this->trackDuplicateKeys( $key );
-
- return $this->resolveSegments( $key, $this->doGet( $key, $flags ) );
- }
-
- /**
- * Track the number of times that a given key has been used.
- * @param string $key
- */
- private function trackDuplicateKeys( $key ) {
- if ( !$this->reportDupes ) {
- return;
- }
-
- if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
- // Track that we have seen this key. This N-1 counting style allows
- // easy filtering with array_filter() later.
- $this->duplicateKeyLookups[$key] = 0;
- } else {
- $this->duplicateKeyLookups[$key] += 1;
-
- if ( $this->dupeTrackScheduled === false ) {
- $this->dupeTrackScheduled = true;
- // Schedule a callback that logs keys processed more than once by get().
- call_user_func( $this->asyncHandler, function () {
- $dups = array_filter( $this->duplicateKeyLookups );
- foreach ( $dups as $key => $count ) {
- $this->logger->warning(
- 'Duplicate get(): "{key}" fetched {count} times',
- // Count is N-1 of the actual lookup count
- [ 'key' => $key, 'count' => $count + 1, ]
- );
- }
- } );
- }
- }
- }
-
- /**
- * @param string $key
- * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
- * @param mixed|null &$casToken Token to use for check-and-set comparisons
- * @return mixed Returns false on failure or if the item does not exist
- */
- abstract protected function doGet( $key, $flags = 0, &$casToken = null );
+ abstract public function get( $key, $flags = 0 );
/**
* Set an item
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- public function set( $key, $value, $exptime = 0, $flags = 0 ) {
- if (
- is_int( $value ) || // avoid breaking incr()/decr()
- ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS ||
- is_infinite( $this->segmentationSize )
- ) {
- return $this->doSet( $key, $value, $exptime, $flags );
- }
-
- $serialized = $this->serialize( $value );
- $segmentSize = $this->getSegmentationSize();
- $maxTotalSize = $this->getSegmentedValueMaxSize();
-
- $size = strlen( $serialized );
- if ( $size <= $segmentSize ) {
- // Since the work of serializing it was already done, just use it inline
- return $this->doSet(
- $key,
- SerializedValueContainer::newUnified( $serialized ),
- $exptime,
- $flags
- );
- } elseif ( $size > $maxTotalSize ) {
- $this->setLastError( "Key $key exceeded $maxTotalSize bytes." );
-
- return false;
- }
-
- $chunksByKey = [];
- $segmentHashes = [];
- $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 );
- for ( $i = 0; $i < $count; ++$i ) {
- $segment = substr( $serialized, $i * $segmentSize, $segmentSize );
- $hash = sha1( $segment );
- $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash );
- $chunksByKey[$chunkKey] = $segment;
- $segmentHashes[] = $hash;
- }
-
- $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity
- $ok = $this->setMulti( $chunksByKey, $exptime, $flags );
- if ( $ok ) {
- // Only when all segments are stored should the main key be changed
- $ok = $this->doSet(
- $key,
- SerializedValueContainer::newSegmented( $segmentHashes ),
- $exptime,
- $flags
- );
- }
-
- return $ok;
- }
-
- /**
- * Set an item
- *
- * @param string $key
- * @param mixed $value
- * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- * @return bool Success
- */
- abstract protected function doSet( $key, $value, $exptime = 0, $flags = 0 );
+ abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
/**
* Delete an item
* @return bool True if the item was deleted or not found, false on failure
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
*/
- public function delete( $key, $flags = 0 ) {
- if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) {
- return $this->doDelete( $key, $flags );
- }
-
- $mainValue = $this->doGet( $key, self::READ_LATEST );
- if ( !$this->doDelete( $key, $flags ) ) {
- return false;
- }
-
- if ( !SerializedValueContainer::isSegmented( $mainValue ) ) {
- return true; // no segments to delete
- }
-
- $orderedKeys = array_map(
- function ( $segmentHash ) use ( $key ) {
- return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
- },
- $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
- );
-
- return $this->deleteMulti( $orderedKeys, $flags );
- }
-
- /**
- * Delete an item
- *
- * @param string $key
- * @return bool True if the item was deleted or not found, false on failure
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- */
- abstract protected function doDelete( $key, $flags = 0 );
+ abstract public function delete( $key, $flags = 0 );
/**
* Insert an item if it does not already exist
* @return bool Success
* @throws InvalidArgumentException
*/
- public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
- return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
- }
-
- /**
- * @see BagOStuff::merge()
- *
- * @param string $key
- * @param callable $callback Callback method to be executed
- * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
- * @param int $attempts The amount of times to attempt a merge in case of failure
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- * @return bool Success
- */
- final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
- do {
- $casToken = null; // passed by reference
- // Get the old value and CAS token from cache
- $this->clearLastError();
- $currentValue = $this->resolveSegments(
- $key,
- $this->doGet( $key, self::READ_LATEST, $casToken )
- );
- if ( $this->getLastError() ) {
- $this->logger->warning(
- __METHOD__ . ' failed due to I/O error on get() for {key}.',
- [ 'key' => $key ]
- );
-
- return false; // don't spam retries (retry only on races)
- }
-
- // Derive the new value from the old value
- $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
- $hadNoCurrentValue = ( $currentValue === false );
- unset( $currentValue ); // free RAM in case the value is large
-
- $this->clearLastError();
- if ( $value === false ) {
- $success = true; // do nothing
- } elseif ( $hadNoCurrentValue ) {
- // Try to create the key, failing if it gets created in the meantime
- $success = $this->add( $key, $value, $exptime, $flags );
- } else {
- // Try to update the key, failing if it gets changed in the meantime
- $success = $this->cas( $casToken, $key, $value, $exptime, $flags );
- }
- if ( $this->getLastError() ) {
- $this->logger->warning(
- __METHOD__ . ' failed due to I/O error for {key}.',
- [ 'key' => $key ]
- );
-
- return false; // IO error; don't spam retries
- }
-
- } while ( !$success && --$attempts );
-
- return $success;
- }
-
- /**
- * Check and set an item
- *
- * @param mixed $casToken
- * @param string $key
- * @param mixed $value
- * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- * @return bool Success
- */
- protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
- if ( !$this->lock( $key, 0 ) ) {
- return false; // non-blocking
- }
-
- $curCasToken = null; // passed by reference
- $this->doGet( $key, self::READ_LATEST, $curCasToken );
- if ( $casToken === $curCasToken ) {
- $success = $this->set( $key, $value, $exptime, $flags );
- } else {
- $this->logger->info(
- __METHOD__ . ' failed due to race condition for {key}.',
- [ 'key' => $key ]
- );
-
- $success = false; // mismatched or failed
- }
-
- $this->unlock( $key );
-
- return $success;
- }
+ abstract public function merge(
+ $key,
+ callable $callback,
+ $exptime = 0,
+ $attempts = 10,
+ $flags = 0
+ );
/**
* Change the expiration on a key if it exists
* @return bool Success Returns false on failure or if the item does not exist
* @since 1.28
*/
- public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
- return $this->doChangeTTL( $key, $exptime, $flags );
- }
-
- /**
- * @param string $key
- * @param int $exptime
- * @param int $flags
- * @return bool
- */
- protected function doChangeTTL( $key, $exptime, $flags ) {
- $expiry = $this->convertToExpiry( $exptime );
- $delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
-
- if ( !$this->lock( $key, 0 ) ) {
- return false;
- }
- // Use doGet() to avoid having to trigger resolveSegments()
- $blob = $this->doGet( $key, self::READ_LATEST );
- if ( $blob ) {
- if ( $delete ) {
- $ok = $this->doDelete( $key, $flags );
- } else {
- $ok = $this->doSet( $key, $blob, $exptime, $flags );
- }
- } else {
- $ok = false;
- }
-
- $this->unlock( $key );
-
- return $ok;
- }
+ abstract public function changeTTL( $key, $exptime = 0, $flags = 0 );
/**
* Acquire an advisory lock on a key string
* @param string $rclass Allow reentry if set and the current lock used this value
* @return bool Success
*/
- public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
- // Avoid deadlocks and allow lock reentry if specified
- if ( isset( $this->locks[$key] ) ) {
- if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
- ++$this->locks[$key]['depth'];
- return true;
- } else {
- return false;
- }
- }
-
- $fname = __METHOD__;
- $expiry = min( $expiry ?: INF, self::TTL_DAY );
- $loop = new WaitConditionLoop(
- function () use ( $key, $expiry, $fname ) {
- $this->clearLastError();
- if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
- return WaitConditionLoop::CONDITION_REACHED; // locked!
- } elseif ( $this->getLastError() ) {
- $this->logger->warning(
- $fname . ' failed due to I/O error for {key}.',
- [ 'key' => $key ]
- );
-
- return WaitConditionLoop::CONDITION_ABORTED; // network partition?
- }
-
- return WaitConditionLoop::CONDITION_CONTINUE;
- },
- $timeout
- );
-
- $code = $loop->invoke();
- $locked = ( $code === $loop::CONDITION_REACHED );
- if ( $locked ) {
- $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
- } elseif ( $code === $loop::CONDITION_TIMED_OUT ) {
- $this->logger->warning(
- "$fname failed due to timeout for {key}.",
- [ 'key' => $key, 'timeout' => $timeout ]
- );
- }
-
- return $locked;
- }
+ abstract public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' );
/**
* Release an advisory lock on a key string
* @param string $key
* @return bool Success
*/
- public function unlock( $key ) {
- if ( !isset( $this->locks[$key] ) ) {
- return false;
- }
-
- if ( --$this->locks[$key]['depth'] <= 0 ) {
- unset( $this->locks[$key] );
-
- $ok = $this->doDelete( "{$key}:lock" );
- if ( !$ok ) {
- $this->logger->warning(
- __METHOD__ . ' failed to release lock for {key}.',
- [ 'key' => $key ]
- );
- }
-
- return $ok;
- }
-
- return true;
- }
+ abstract public function unlock( $key );
/**
* Get a lightweight exclusive self-unlocking lock
*
* @return bool Success; false if unimplemented
*/
- public function deleteObjectsExpiringBefore(
+ abstract public function deleteObjectsExpiringBefore(
$timestamp,
callable $progress = null,
$limit = INF
- ) {
- return false;
- }
-
- /**
- * Get an associative array containing the item for each of the keys that have items.
- * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
- * @param int $flags Bitfield; supports READ_LATEST [optional]
- * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
- */
- public function getMulti( array $keys, $flags = 0 ) {
- $foundByKey = $this->doGetMulti( $keys, $flags );
-
- $res = [];
- foreach ( $keys as $key ) {
- // Resolve one blob at a time (avoids too much I/O at once)
- 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 $res;
- }
+ );
/**
* Get an associative array containing the item for each of the keys that have items.
* @param int $flags Bitfield; supports READ_LATEST [optional]
* @return mixed[] Map of (key => value) for existing keys
*/
- protected function doGetMulti( array $keys, $flags = 0 ) {
- $res = [];
- foreach ( $keys as $key ) {
- $val = $this->doGet( $key, $flags );
- if ( $val !== false ) {
- $res[$key] = $val;
- }
- }
-
- return $res;
- }
+ abstract public function getMulti( array $keys, $flags = 0 );
/**
* Batch insertion/replace
*
* This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
*
+ * WRITE_BACKGROUND can be used for bulk insertion where the response is not vital
+ *
* @param mixed[] $data Map of (key => value)
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
* @since 1.24
*/
- public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
- if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
- throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
- }
- return $this->doSetMulti( $data, $exptime, $flags );
- }
-
- /**
- * @param mixed[] $data Map of (key => value)
- * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- * @return bool Success
- */
- protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
- $res = true;
- foreach ( $data as $key => $value ) {
- $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
- }
- return $res;
- }
+ abstract public function setMulti( array $data, $exptime = 0, $flags = 0 );
/**
* Batch deletion
*
* This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
*
+ * WRITE_BACKGROUND can be used for bulk deletion where the response is not vital
+ *
* @param string[] $keys List of keys
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
* @since 1.33
*/
- public function deleteMulti( array $keys, $flags = 0 ) {
- if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
- throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
- }
- return $this->doDeleteMulti( $keys, $flags );
- }
-
- /**
- * @param string[] $keys List of keys
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants
- * @return bool Success
- */
- protected function doDeleteMulti( array $keys, $flags = 0 ) {
- $res = true;
- foreach ( $keys as $key ) {
- $res = $this->doDelete( $key, $flags ) && $res;
- }
- return $res;
- }
+ abstract public function deleteMulti( array $keys, $flags = 0 );
/**
* Change the expiration of multiple keys that exist
* @return bool Success
* @since 1.34
*/
- public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
- $res = true;
- foreach ( $keys as $key ) {
- $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res;
- }
-
- return $res;
- }
+ abstract public function changeTTLMulti( array $keys, $exptime, $flags = 0 );
/**
* Increase stored value of $key by $value while preserving its TTL
* @param int $value Value to subtract from $key (default: 1) [optional]
* @return int|bool New value or false on failure
*/
- public function decr( $key, $value = 1 ) {
- return $this->incr( $key, - $value );
- }
+ abstract public function decr( $key, $value = 1 );
/**
* Increase stored value of $key by $value while preserving its TTL
* @return int|bool New value or false on failure
* @since 1.24
*/
- public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
- $this->clearLastError();
- $newValue = $this->incr( $key, $value );
- if ( $newValue === false && !$this->getLastError() ) {
- // No key set; initialize
- $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
- if ( $newValue === false && !$this->getLastError() ) {
- // Raced out initializing; increment
- $newValue = $this->incr( $key, $value );
- }
- }
-
- return $newValue;
- }
-
- /**
- * Get and reassemble the chunks of blob at the given key
- *
- * @param string $key
- * @param mixed $mainValue
- * @return string|null|bool The combined string, false if missing, null on error
- */
- final protected function resolveSegments( $key, $mainValue ) {
- if ( SerializedValueContainer::isUnified( $mainValue ) ) {
- return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
- }
-
- if ( SerializedValueContainer::isSegmented( $mainValue ) ) {
- $orderedKeys = array_map(
- function ( $segmentHash ) use ( $key ) {
- return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
- },
- $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
- );
-
- $segmentsByKey = $this->doGetMulti( $orderedKeys );
-
- $parts = [];
- foreach ( $orderedKeys as $segmentKey ) {
- if ( isset( $segmentsByKey[$segmentKey] ) ) {
- $parts[] = $segmentsByKey[$segmentKey];
- } else {
- return false; // missing segment
- }
- }
-
- return $this->unserialize( implode( '', $parts ) );
- }
-
- return $mainValue;
- }
+ abstract public function incrWithInit( $key, $ttl, $value = 1, $init = 1 );
/**
* Get the "last error" registered; clearLastError() should be called manually
* @return int ERR_* constant for the "last error" registry
* @since 1.23
*/
- public function getLastError() {
- return $this->lastError;
- }
+ abstract public function getLastError();
/**
* Clear the "last error" registry
* @since 1.23
*/
- public function clearLastError() {
- $this->lastError = self::ERR_NONE;
- }
-
- /**
- * Set the "last error" registry
- * @param int $err ERR_* constant
- * @since 1.23
- */
- protected function setLastError( $err ) {
- $this->lastError = $err;
- }
+ abstract public function clearLastError();
/**
* Let a callback be run to avoid wasting time on special blocking calls
* @param callable $workCallback
* @since 1.28
*/
- final public function addBusyCallback( callable $workCallback ) {
- $this->busyCallbacks[] = $workCallback;
- }
-
- /**
- * @param string $text
- */
- protected function debug( $text ) {
- if ( $this->debugMode ) {
- $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
- }
- }
-
- /**
- * @param int $exptime
- * @return bool
- */
- final protected function expiryIsRelative( $exptime ) {
- return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
- }
-
- /**
- * Convert an optionally relative timestamp to an absolute time
- *
- * The input value will be cast to an integer and interpreted as follows:
- * - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
- * - negative: relative TTL; return UNIX timestamp offset by this value
- * - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
- * - positive (>= 10 years): absolute UNIX timestamp; return this value
- *
- * @param int $exptime Absolute TTL or 0 for indefinite
- * @return int
- */
- final protected function convertToExpiry( $exptime ) {
- return $this->expiryIsRelative( $exptime )
- ? (int)$this->getCurrentTime() + $exptime
- : $exptime;
- }
-
- /**
- * Convert an optionally absolute expiry time to a relative time. If an
- * absolute time is specified which is in the past, use a short expiry time.
- *
- * @param int $exptime
- * @return int
- */
- final protected function convertToRelative( $exptime ) {
- return $this->expiryIsRelative( $exptime )
- ? (int)$exptime
- : max( $exptime - (int)$this->getCurrentTime(), 1 );
- }
-
- /**
- * Check if a value is an integer
- *
- * @param mixed $value
- * @return bool
- */
- final protected function isInteger( $value ) {
- if ( is_int( $value ) ) {
- return true;
- } elseif ( !is_string( $value ) ) {
- return false;
- }
-
- $integer = (int)$value;
-
- return ( $value === (string)$integer );
- }
+ abstract public function addBusyCallback( callable $workCallback );
/**
* Construct a cache key.
* @param array $args
* @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
- public function makeKeyInternal( $keyspace, $args ) {
- $key = $keyspace;
- foreach ( $args as $arg ) {
- $key .= ':' . str_replace( ':', '%3A', $arg );
- }
- return strtr( $key, ' ', '_' );
- }
+ abstract public function makeKeyInternal( $keyspace, $args );
/**
* Make a global cache key.
* @param string|null $component [optional] Key component (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
- public function makeGlobalKey( $class, $component = null ) {
- return $this->makeKeyInternal( 'global', func_get_args() );
- }
+ abstract public function makeGlobalKey( $class, $component = null );
/**
* Make a cache key, scoped to this instance's keyspace.
* @param string|null $component [optional] Key component (starting with a key collection name)
* @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
- public function makeKey( $class, $component = null ) {
- return $this->makeKeyInternal( $this->keyspace, func_get_args() );
- }
+ abstract public function makeKey( $class, $component = null );
/**
* @param int $flag ATTR_* class constant
* @since 1.34
*/
public function getSegmentationSize() {
- return $this->segmentationSize;
+ return INF;
}
/**
* @since 1.34
*/
public function getSegmentedValueMaxSize() {
- return $this->segmentedValueMaxSize;
+ return INF;
}
/**
public function setMockTime( &$time ) {
$this->wallClockOverride =& $time;
}
-
- /**
- * @param mixed $value
- * @return string|int String/integer representation
- * @note Special handling is usually needed for integers so incr()/decr() work
- */
- protected function serialize( $value ) {
- return is_int( $value ) ? $value : serialize( $value );
- }
-
- /**
- * @param string|int $value
- * @return mixed Original value or false on error
- * @note Special handling is usually needed for integers so incr()/decr() work
- */
- protected function unserialize( $value ) {
- return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
- }
}
* @param array $params Parameters for HashBagOStuff
*/
public function __construct( BagOStuff $backend, $params = [] ) {
- unset( $params['reportDupes'] ); // useless here
-
parent::__construct( $params );
$this->backend = $backend;
$this->attrMap = $backend->attrMap;
}
- protected function doGet( $key, $flags = 0, &$casToken = null ) {
- $ret = $this->procCache->get( $key, $flags );
- if ( $ret === false && !$this->procCache->hasKey( $key ) ) {
- $ret = $this->backend->get( $key, $flags );
- $this->set( $key, $ret, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
+ public function setDebug( $enabled ) {
+ parent::setDebug( $enabled );
+ $this->backend->setDebug( $enabled );
+ }
+
+ public function get( $key, $flags = 0 ) {
+ $value = $this->procCache->get( $key, $flags );
+ if ( $value === false && !$this->procCache->hasKey( $key ) ) {
+ $value = $this->backend->get( $key, $flags );
+ $this->set( $key, $value, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
+ }
+
+ return $value;
+ }
+
+ public function getMulti( array $keys, $flags = 0 ) {
+ $valuesByKeyCached = [];
+
+ $keysMissing = [];
+ foreach ( $keys as $key ) {
+ $value = $this->procCache->get( $key, $flags );
+ if ( $value === false && !$this->procCache->hasKey( $key ) ) {
+ $keysMissing[] = $key;
+ } else {
+ $valuesByKeyCached[$key] = $value;
+ }
}
- return $ret;
+ $valuesByKeyFetched = $this->backend->getMulti( $keys, $flags );
+ $this->setMulti( $valuesByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
+
+ return $valuesByKeyCached + $valuesByKeyFetched;
}
- protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->procCache->set( $key, $value, $exptime, $flags );
if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
$this->backend->set( $key, $value, $exptime, $flags );
return true;
}
- protected function doDelete( $key, $flags = 0 ) {
+ public function delete( $key, $flags = 0 ) {
$this->procCache->delete( $key, $flags );
if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
$this->backend->delete( $key, $flags );
return true;
}
- public function deleteObjectsExpiringBefore(
- $timestamp,
- callable $progress = null,
- $limit = INF
- ) {
- $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
-
- return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
- }
-
- // These just call the backend (tested elsewhere)
- // @codeCoverageIgnoreStart
-
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
if ( $this->get( $key ) === false ) {
return $this->set( $key, $value, $exptime, $flags );
return false; // key already set
}
- public function incr( $key, $value = 1 ) {
- $n = $this->backend->incr( $key, $value );
+ // These just call the backend (tested elsewhere)
+ // @codeCoverageIgnoreStart
+ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
$this->procCache->delete( $key );
- return $n;
+ return $this->backend->merge( $key, $callback, $exptime, $attempts, $flags );
+ }
+
+ public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+ $this->procCache->delete( $key );
+
+ return $this->backend->changeTTL( $key, $exptime, $flags );
}
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
return $this->backend->unlock( $key );
}
+ public function deleteObjectsExpiringBefore(
+ $timestamp,
+ callable $progress = null,
+ $limit = INF
+ ) {
+ $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
+
+ return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
+ }
+
public function makeKeyInternal( $keyspace, $args ) {
return $this->backend->makeKeyInternal( ...func_get_args() );
}
return $this->backend->makeGlobalKey( ...func_get_args() );
}
- public function setDebug( $bool ) {
- parent::setDebug( $bool );
- $this->backend->setDebug( $bool );
- }
-
public function getLastError() {
return $this->backend->getLastError();
}
return $this->backend->clearLastError();
}
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+ $this->procCache->setMulti( $data, $exptime, $flags );
+ if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+ return $this->backend->setMulti( $data, $exptime, $flags );
+ }
+
+ return true;
+ }
+
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ $this->procCache->deleteMulti( $keys, $flags );
+ if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+ return $this->backend->deleteMulti( $keys, $flags );
+ }
+
+ return true;
+ }
+
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ $this->procCache->changeTTLMulti( $keys, $exptime, $flags );
+ if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+ return $this->backend->changeTTLMulti( $keys, $exptime, $flags );
+ }
+
+ return true;
+ }
+
+ public function incr( $key, $value = 1 ) {
+ $this->procCache->delete( $key );
+
+ return $this->backend->incr( $key, $value );
+ }
+
+ public function decr( $key, $value = 1 ) {
+ $this->procCache->delete( $key );
+
+ return $this->backend->decr( $key, $value );
+ }
+
+ public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+ $this->procCache->delete( $key );
+
+ return $this->backend->incrWithInit( $key, $ttl, $value, $init );
+ }
+
+ public function addBusyCallback( callable $workCallback ) {
+ $this->backend->addBusyCallback( $workCallback );
+ }
+
+ public function setMockTime( &$time ) {
+ parent::setMockTime( $time );
+ $this->procCache->setMockTime( $time );
+ $this->backend->setMockTime( $time );
+ }
+
// @codeCoverageIgnoreEnd
}
*
* @ingroup Cache
*/
-class EmptyBagOStuff extends BagOStuff {
+class EmptyBagOStuff extends MediumSpecificBagOStuff {
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$casToken = null;
*
* @ingroup Cache
*/
-class HashBagOStuff extends BagOStuff {
+class HashBagOStuff extends MediumSpecificBagOStuff {
/** @var mixed[] */
protected $bag = [];
/** @var int Max entries allowed */
--- /dev/null
+<?php
+/**
+ * Storage medium specific cache for storing items.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * Storage medium specific cache for storing items (e.g. redis, memcached, ...)
+ *
+ * This should not be used for proxy classes that simply wrap other cache instances
+ *
+ * @ingroup Cache
+ * @since 1.34
+ */
+abstract class MediumSpecificBagOStuff extends BagOStuff {
+ /** @var array[] Lock tracking */
+ protected $locks = [];
+ /** @var int ERR_* class constant */
+ protected $lastError = self::ERR_NONE;
+ /** @var string */
+ protected $keyspace = 'local';
+ /** @var int Seconds */
+ protected $syncTimeout;
+ /** @var int Bytes; chunk size of segmented cache values */
+ protected $segmentationSize;
+ /** @var int Bytes; maximum total size of a segmented cache value */
+ protected $segmentedValueMaxSize;
+
+ /** @var array */
+ private $duplicateKeyLookups = [];
+ /** @var bool */
+ private $reportDupes = false;
+ /** @var bool */
+ private $dupeTrackScheduled = false;
+
+ /** @var callable[] */
+ protected $busyCallbacks = [];
+
+ /** @var string Component to use for key construction of blob segment keys */
+ const SEGMENT_COMPONENT = 'segment';
+
+ /**
+ * @see BagOStuff::__construct()
+ * Additional $params options include:
+ * - logger: Psr\Log\LoggerInterface instance
+ * - keyspace: Default keyspace for $this->makeKey()
+ * - reportDupes: Whether to emit warning log messages for all keys that were
+ * requested more than once (requires an asyncHandler).
+ * - syncTimeout: How long to wait with WRITE_SYNC in seconds.
+ * - segmentationSize: The chunk size, in bytes, of segmented values. The value should
+ * not exceed the maximum size of values in the storage backend, as configured by
+ * the site administrator.
+ * - segmentedValueMaxSize: The maximum total size, in bytes, of segmented values.
+ * This should be configured to a reasonable size give the site traffic and the
+ * amount of I/O between application and cache servers that the network can handle.
+ * @param array $params
+ */
+ public function __construct( array $params = [] ) {
+ parent::__construct( $params );
+
+ if ( isset( $params['keyspace'] ) ) {
+ $this->keyspace = $params['keyspace'];
+ }
+
+ if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
+ $this->reportDupes = true;
+ }
+
+ $this->syncTimeout = $params['syncTimeout'] ?? 3;
+ $this->segmentationSize = $params['segmentationSize'] ?? 8388608; // 8MiB
+ $this->segmentedValueMaxSize = $params['segmentedValueMaxSize'] ?? 67108864; // 64MiB
+ }
+
+ /**
+ * Get an item with the given key
+ *
+ * If the key includes a deterministic input hash (e.g. the key can only have
+ * the correct value) or complete staleness checks are handled by the caller
+ * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
+ * This lets tiered backends know they can safely upgrade a cached value to
+ * higher tiers using standard TTLs.
+ *
+ * @param string $key
+ * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
+ * @return mixed Returns false on failure or if the item does not exist
+ */
+ public function get( $key, $flags = 0 ) {
+ $this->trackDuplicateKeys( $key );
+
+ return $this->resolveSegments( $key, $this->doGet( $key, $flags ) );
+ }
+
+ /**
+ * Track the number of times that a given key has been used.
+ * @param string $key
+ */
+ private function trackDuplicateKeys( $key ) {
+ if ( !$this->reportDupes ) {
+ return;
+ }
+
+ if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
+ // Track that we have seen this key. This N-1 counting style allows
+ // easy filtering with array_filter() later.
+ $this->duplicateKeyLookups[$key] = 0;
+ } else {
+ $this->duplicateKeyLookups[$key] += 1;
+
+ if ( $this->dupeTrackScheduled === false ) {
+ $this->dupeTrackScheduled = true;
+ // Schedule a callback that logs keys processed more than once by get().
+ call_user_func( $this->asyncHandler, function () {
+ $dups = array_filter( $this->duplicateKeyLookups );
+ foreach ( $dups as $key => $count ) {
+ $this->logger->warning(
+ 'Duplicate get(): "{key}" fetched {count} times',
+ // Count is N-1 of the actual lookup count
+ [ 'key' => $key, 'count' => $count + 1, ]
+ );
+ }
+ } );
+ }
+ }
+ }
+
+ /**
+ * @param string $key
+ * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
+ * @param mixed|null &$casToken Token to use for check-and-set comparisons
+ * @return mixed Returns false on failure or if the item does not exist
+ */
+ abstract protected function doGet( $key, $flags = 0, &$casToken = null );
+
+ /**
+ * Set an item
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+ if (
+ is_int( $value ) || // avoid breaking incr()/decr()
+ ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS ||
+ is_infinite( $this->segmentationSize )
+ ) {
+ return $this->doSet( $key, $value, $exptime, $flags );
+ }
+
+ $serialized = $this->serialize( $value );
+ $segmentSize = $this->getSegmentationSize();
+ $maxTotalSize = $this->getSegmentedValueMaxSize();
+
+ $size = strlen( $serialized );
+ if ( $size <= $segmentSize ) {
+ // Since the work of serializing it was already done, just use it inline
+ return $this->doSet(
+ $key,
+ SerializedValueContainer::newUnified( $serialized ),
+ $exptime,
+ $flags
+ );
+ } elseif ( $size > $maxTotalSize ) {
+ $this->setLastError( "Key $key exceeded $maxTotalSize bytes." );
+
+ return false;
+ }
+
+ $chunksByKey = [];
+ $segmentHashes = [];
+ $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 );
+ for ( $i = 0; $i < $count; ++$i ) {
+ $segment = substr( $serialized, $i * $segmentSize, $segmentSize );
+ $hash = sha1( $segment );
+ $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash );
+ $chunksByKey[$chunkKey] = $segment;
+ $segmentHashes[] = $hash;
+ }
+
+ $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity
+ $ok = $this->setMulti( $chunksByKey, $exptime, $flags );
+ if ( $ok ) {
+ // Only when all segments are stored should the main key be changed
+ $ok = $this->doSet(
+ $key,
+ SerializedValueContainer::newSegmented( $segmentHashes ),
+ $exptime,
+ $flags
+ );
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Set an item
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ abstract protected function doSet( $key, $value, $exptime = 0, $flags = 0 );
+
+ /**
+ * Delete an item
+ *
+ * For large values written using WRITE_ALLOW_SEGMENTS, this only deletes the main
+ * segment list key unless WRITE_PRUNE_SEGMENTS is in the flags. While deleting the segment
+ * list key has the effect of functionally deleting the key, it leaves unused blobs in cache.
+ *
+ * @param string $key
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool True if the item was deleted or not found, false on failure
+ */
+ public function delete( $key, $flags = 0 ) {
+ if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) {
+ return $this->doDelete( $key, $flags );
+ }
+
+ $mainValue = $this->doGet( $key, self::READ_LATEST );
+ if ( !$this->doDelete( $key, $flags ) ) {
+ return false;
+ }
+
+ if ( !SerializedValueContainer::isSegmented( $mainValue ) ) {
+ return true; // no segments to delete
+ }
+
+ $orderedKeys = array_map(
+ function ( $segmentHash ) use ( $key ) {
+ return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
+ },
+ $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
+ );
+
+ return $this->deleteMulti( $orderedKeys, $flags );
+ }
+
+ /**
+ * Delete an item
+ *
+ * @param string $key
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool True if the item was deleted or not found, false on failure
+ */
+ abstract protected function doDelete( $key, $flags = 0 );
+
+ /**
+ * Merge changes into the existing cache value (possibly creating a new one)
+ *
+ * The callback function returns the new value given the current value
+ * (which will be false if not present), and takes the arguments:
+ * (this BagOStuff, cache key, current value, TTL).
+ * The TTL parameter is reference set to $exptime. It can be overriden in the callback.
+ * Nothing is stored nor deleted if the callback returns false.
+ *
+ * @param string $key
+ * @param callable $callback Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ * @throws InvalidArgumentException
+ */
+ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+ return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
+ }
+
+ /**
+ * @param string $key
+ * @param callable $callback Callback method to be executed
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $attempts The amount of times to attempt a merge in case of failure
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ * @see BagOStuff::merge()
+ *
+ */
+ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
+ do {
+ $casToken = null; // passed by reference
+ // Get the old value and CAS token from cache
+ $this->clearLastError();
+ $currentValue = $this->resolveSegments(
+ $key,
+ $this->doGet( $key, self::READ_LATEST, $casToken )
+ );
+ if ( $this->getLastError() ) {
+ $this->logger->warning(
+ __METHOD__ . ' failed due to I/O error on get() for {key}.',
+ [ 'key' => $key ]
+ );
+
+ return false; // don't spam retries (retry only on races)
+ }
+
+ // Derive the new value from the old value
+ $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
+ $hadNoCurrentValue = ( $currentValue === false );
+ unset( $currentValue ); // free RAM in case the value is large
+
+ $this->clearLastError();
+ if ( $value === false ) {
+ $success = true; // do nothing
+ } elseif ( $hadNoCurrentValue ) {
+ // Try to create the key, failing if it gets created in the meantime
+ $success = $this->add( $key, $value, $exptime, $flags );
+ } else {
+ // Try to update the key, failing if it gets changed in the meantime
+ $success = $this->cas( $casToken, $key, $value, $exptime, $flags );
+ }
+ if ( $this->getLastError() ) {
+ $this->logger->warning(
+ __METHOD__ . ' failed due to I/O error for {key}.',
+ [ 'key' => $key ]
+ );
+
+ return false; // IO error; don't spam retries
+ }
+
+ } while ( !$success && --$attempts );
+
+ return $success;
+ }
+
+ /**
+ * Check and set an item
+ *
+ * @param mixed $casToken
+ * @param string $key
+ * @param mixed $value
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( !$this->lock( $key, 0 ) ) {
+ return false; // non-blocking
+ }
+
+ $curCasToken = null; // passed by reference
+ $this->doGet( $key, self::READ_LATEST, $curCasToken );
+ if ( $casToken === $curCasToken ) {
+ $success = $this->set( $key, $value, $exptime, $flags );
+ } else {
+ $this->logger->info(
+ __METHOD__ . ' failed due to race condition for {key}.',
+ [ 'key' => $key ]
+ );
+
+ $success = false; // mismatched or failed
+ }
+
+ $this->unlock( $key );
+
+ return $success;
+ }
+
+ /**
+ * Change the expiration on a key if it exists
+ *
+ * If an expiry in the past is given then the key will immediately be expired
+ *
+ * For large values written using WRITE_ALLOW_SEGMENTS, this only changes the TTL of the
+ * main segment list key. While lowering the TTL of the segment list key has the effect of
+ * functionally lowering the TTL of the key, it might leave unused blobs in cache for longer.
+ * Raising the TTL of such keys is not effective, since the expiration of a single segment
+ * key effectively expires the entire value.
+ *
+ * @param string $key
+ * @param int $exptime TTL or UNIX timestamp
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+ * @return bool Success Returns false on failure or if the item does not exist
+ * @since 1.28
+ */
+ public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+ return $this->doChangeTTL( $key, $exptime, $flags );
+ }
+
+ /**
+ * @param string $key
+ * @param int $exptime
+ * @param int $flags
+ * @return bool
+ */
+ protected function doChangeTTL( $key, $exptime, $flags ) {
+ $expiry = $this->convertToExpiry( $exptime );
+ $delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
+
+ if ( !$this->lock( $key, 0 ) ) {
+ return false;
+ }
+ // Use doGet() to avoid having to trigger resolveSegments()
+ $blob = $this->doGet( $key, self::READ_LATEST );
+ if ( $blob ) {
+ if ( $delete ) {
+ $ok = $this->doDelete( $key, $flags );
+ } else {
+ $ok = $this->doSet( $key, $blob, $exptime, $flags );
+ }
+ } else {
+ $ok = false;
+ }
+
+ $this->unlock( $key );
+
+ return $ok;
+ }
+
+ /**
+ * Acquire an advisory lock on a key string
+ *
+ * Note that if reentry is enabled, duplicate calls ignore $expiry
+ *
+ * @param string $key
+ * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
+ * @param int $expiry Lock expiry [optional]; 1 day maximum
+ * @param string $rclass Allow reentry if set and the current lock used this value
+ * @return bool Success
+ */
+ public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
+ // Avoid deadlocks and allow lock reentry if specified
+ if ( isset( $this->locks[$key] ) ) {
+ if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
+ ++$this->locks[$key]['depth'];
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ $fname = __METHOD__;
+ $expiry = min( $expiry ?: INF, self::TTL_DAY );
+ $loop = new WaitConditionLoop(
+ function () use ( $key, $expiry, $fname ) {
+ $this->clearLastError();
+ if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
+ return WaitConditionLoop::CONDITION_REACHED; // locked!
+ } elseif ( $this->getLastError() ) {
+ $this->logger->warning(
+ $fname . ' failed due to I/O error for {key}.',
+ [ 'key' => $key ]
+ );
+
+ return WaitConditionLoop::CONDITION_ABORTED; // network partition?
+ }
+
+ return WaitConditionLoop::CONDITION_CONTINUE;
+ },
+ $timeout
+ );
+
+ $code = $loop->invoke();
+ $locked = ( $code === $loop::CONDITION_REACHED );
+ if ( $locked ) {
+ $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
+ } elseif ( $code === $loop::CONDITION_TIMED_OUT ) {
+ $this->logger->warning(
+ "$fname failed due to timeout for {key}.",
+ [ 'key' => $key, 'timeout' => $timeout ]
+ );
+ }
+
+ return $locked;
+ }
+
+ /**
+ * Release an advisory lock on a key string
+ *
+ * @param string $key
+ * @return bool Success
+ */
+ public function unlock( $key ) {
+ if ( !isset( $this->locks[$key] ) ) {
+ return false;
+ }
+
+ if ( --$this->locks[$key]['depth'] <= 0 ) {
+ unset( $this->locks[$key] );
+
+ $ok = $this->doDelete( "{$key}:lock" );
+ if ( !$ok ) {
+ $this->logger->warning(
+ __METHOD__ . ' failed to release lock for {key}.',
+ [ 'key' => $key ]
+ );
+ }
+
+ return $ok;
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete all objects expiring before a certain date.
+ * @param string|int $timestamp The reference date in MW or TS_UNIX format
+ * @param callable|null $progress Optional, a function which will be called
+ * regularly during long-running operations with the percentage progress
+ * as the first parameter. [optional]
+ * @param int $limit Maximum number of keys to delete [default: INF]
+ *
+ * @return bool Success; false if unimplemented
+ */
+ public function deleteObjectsExpiringBefore(
+ $timestamp,
+ callable $progress = null,
+ $limit = INF
+ ) {
+ return false;
+ }
+
+ /**
+ * Get an associative array containing the item for each of the keys that have items.
+ * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
+ * @param int $flags Bitfield; supports READ_LATEST [optional]
+ * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
+ */
+ public function getMulti( array $keys, $flags = 0 ) {
+ $foundByKey = $this->doGetMulti( $keys, $flags );
+
+ $res = [];
+ foreach ( $keys as $key ) {
+ // Resolve one blob at a time (avoids too much I/O at once)
+ 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 $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
+ */
+ protected function doGetMulti( array $keys, $flags = 0 ) {
+ $res = [];
+ foreach ( $keys as $key ) {
+ $val = $this->doGet( $key, $flags );
+ if ( $val !== false ) {
+ $res[$key] = $val;
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * Batch insertion/replace
+ *
+ * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
+ *
+ * @param mixed[] $data Map of (key => value)
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+ * @return bool Success
+ * @since 1.24
+ */
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+ if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+ throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+ }
+ return $this->doSetMulti( $data, $exptime, $flags );
+ }
+
+ /**
+ * @param mixed[] $data Map of (key => value)
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ $res = true;
+ foreach ( $data as $key => $value ) {
+ $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
+ }
+ return $res;
+ }
+
+ /**
+ * Batch deletion
+ *
+ * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
+ *
+ * @param string[] $keys List of keys
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ * @since 1.33
+ */
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+ throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+ }
+ return $this->doDeleteMulti( $keys, $flags );
+ }
+
+ /**
+ * @param string[] $keys List of keys
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
+ $res = true;
+ foreach ( $keys as $key ) {
+ $res = $this->doDelete( $key, $flags ) && $res;
+ }
+ return $res;
+ }
+
+ /**
+ * Change the expiration of multiple keys that exist
+ *
+ * @param string[] $keys List of keys
+ * @param int $exptime TTL or UNIX timestamp
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+ * @return bool Success
+ * @see BagOStuff::changeTTL()
+ *
+ * @since 1.34
+ */
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ $res = true;
+ foreach ( $keys as $key ) {
+ $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res;
+ }
+
+ return $res;
+ }
+
+ /**
+ * Decrease stored value of $key by $value while preserving its TTL
+ * @param string $key
+ * @param int $value Value to subtract from $key (default: 1) [optional]
+ * @return int|bool New value or false on failure
+ */
+ public function decr( $key, $value = 1 ) {
+ return $this->incr( $key, -$value );
+ }
+
+ /**
+ * Increase stored value of $key by $value while preserving its TTL
+ *
+ * This will create the key with value $init and TTL $ttl instead if not present
+ *
+ * @param string $key
+ * @param int $ttl
+ * @param int $value
+ * @param int $init
+ * @return int|bool New value or false on failure
+ * @since 1.24
+ */
+ public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+ $this->clearLastError();
+ $newValue = $this->incr( $key, $value );
+ if ( $newValue === false && !$this->getLastError() ) {
+ // No key set; initialize
+ $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
+ if ( $newValue === false && !$this->getLastError() ) {
+ // Raced out initializing; increment
+ $newValue = $this->incr( $key, $value );
+ }
+ }
+
+ return $newValue;
+ }
+
+ /**
+ * Get and reassemble the chunks of blob at the given key
+ *
+ * @param string $key
+ * @param mixed $mainValue
+ * @return string|null|bool The combined string, false if missing, null on error
+ */
+ final protected function resolveSegments( $key, $mainValue ) {
+ if ( SerializedValueContainer::isUnified( $mainValue ) ) {
+ return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
+ }
+
+ if ( SerializedValueContainer::isSegmented( $mainValue ) ) {
+ $orderedKeys = array_map(
+ function ( $segmentHash ) use ( $key ) {
+ return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
+ },
+ $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
+ );
+
+ $segmentsByKey = $this->doGetMulti( $orderedKeys );
+
+ $parts = [];
+ foreach ( $orderedKeys as $segmentKey ) {
+ if ( isset( $segmentsByKey[$segmentKey] ) ) {
+ $parts[] = $segmentsByKey[$segmentKey];
+ } else {
+ return false; // missing segment
+ }
+ }
+
+ return $this->unserialize( implode( '', $parts ) );
+ }
+
+ return $mainValue;
+ }
+
+ /**
+ * Get the "last error" registered; clearLastError() should be called manually
+ * @return int ERR_* constant for the "last error" registry
+ * @since 1.23
+ */
+ public function getLastError() {
+ return $this->lastError;
+ }
+
+ /**
+ * Clear the "last error" registry
+ * @since 1.23
+ */
+ public function clearLastError() {
+ $this->lastError = self::ERR_NONE;
+ }
+
+ /**
+ * Set the "last error" registry
+ * @param int $err ERR_* constant
+ * @since 1.23
+ */
+ protected function setLastError( $err ) {
+ $this->lastError = $err;
+ }
+
+ /**
+ * Let a callback be run to avoid wasting time on special blocking calls
+ *
+ * The callbacks may or may not be called ever, in any particular order.
+ * They are likely to be invoked when something WRITE_SYNC is used used.
+ * They should follow a caching pattern as shown below, so that any code
+ * using the work will get it's result no matter what happens.
+ * @code
+ * $result = null;
+ * $workCallback = function () use ( &$result ) {
+ * if ( !$result ) {
+ * $result = ....
+ * }
+ * return $result;
+ * }
+ * @endcode
+ *
+ * @param callable $workCallback
+ * @since 1.28
+ */
+ final public function addBusyCallback( callable $workCallback ) {
+ $this->busyCallbacks[] = $workCallback;
+ }
+
+ /**
+ * @param int $exptime
+ * @return bool
+ */
+ final protected function expiryIsRelative( $exptime ) {
+ return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
+ }
+
+ /**
+ * Convert an optionally relative timestamp to an absolute time
+ *
+ * The input value will be cast to an integer and interpreted as follows:
+ * - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
+ * - negative: relative TTL; return UNIX timestamp offset by this value
+ * - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
+ * - positive (>= 10 years): absolute UNIX timestamp; return this value
+ *
+ * @param int $exptime Absolute TTL or 0 for indefinite
+ * @return int
+ */
+ final protected function convertToExpiry( $exptime ) {
+ return $this->expiryIsRelative( $exptime )
+ ? (int)$this->getCurrentTime() + $exptime
+ : $exptime;
+ }
+
+ /**
+ * Convert an optionally absolute expiry time to a relative time. If an
+ * absolute time is specified which is in the past, use a short expiry time.
+ *
+ * @param int $exptime
+ * @return int
+ */
+ final protected function convertToRelative( $exptime ) {
+ return $this->expiryIsRelative( $exptime )
+ ? (int)$exptime
+ : max( $exptime - (int)$this->getCurrentTime(), 1 );
+ }
+
+ /**
+ * Check if a value is an integer
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ final protected function isInteger( $value ) {
+ if ( is_int( $value ) ) {
+ return true;
+ } elseif ( !is_string( $value ) ) {
+ return false;
+ }
+
+ $integer = (int)$value;
+
+ return ( $value === (string)$integer );
+ }
+
+ /**
+ * Construct a cache key.
+ *
+ * @param string $keyspace
+ * @param array $args
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
+ * @since 1.27
+ */
+ public function makeKeyInternal( $keyspace, $args ) {
+ $key = $keyspace;
+ foreach ( $args as $arg ) {
+ $key .= ':' . str_replace( ':', '%3A', $arg );
+ }
+ return strtr( $key, ' ', '_' );
+ }
+
+ /**
+ * Make a global cache key.
+ *
+ * @param string $class Key class
+ * @param string|null $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
+ * @since 1.27
+ */
+ public function makeGlobalKey( $class, $component = null ) {
+ return $this->makeKeyInternal( 'global', func_get_args() );
+ }
+
+ /**
+ * Make a cache key, scoped to this instance's keyspace.
+ *
+ * @param string $class Key class
+ * @param string|null $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
+ * @since 1.27
+ */
+ public function makeKey( $class, $component = null ) {
+ return $this->makeKeyInternal( $this->keyspace, func_get_args() );
+ }
+
+ /**
+ * @param int $flag ATTR_* class constant
+ * @return int QOS_* class constant
+ * @since 1.28
+ */
+ public function getQoS( $flag ) {
+ return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
+ }
+
+ /**
+ * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit)
+ * @since 1.34
+ */
+ public function getSegmentationSize() {
+ return $this->segmentationSize;
+ }
+
+ /**
+ * @return int|float Maximum total segmented object size in bytes (INF for no limit)
+ * @since 1.34
+ */
+ public function getSegmentedValueMaxSize() {
+ return $this->segmentedValueMaxSize;
+ }
+
+ /**
+ * @param mixed $value
+ * @return string|int String/integer representation
+ * @note Special handling is usually needed for integers so incr()/decr() work
+ */
+ protected function serialize( $value ) {
+ return is_int( $value ) ? $value : serialize( $value );
+ }
+
+ /**
+ * @param string|int $value
+ * @return mixed Original value or false on error
+ * @note Special handling is usually needed for integers so incr()/decr() work
+ */
+ protected function unserialize( $value ) {
+ return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
+ }
+
+ /**
+ * @param string $text
+ */
+ protected function debug( $text ) {
+ if ( $this->debugMode ) {
+ $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
+ }
+ }
+}
*
* @ingroup Cache
*/
-abstract class MemcachedBagOStuff extends BagOStuff {
+abstract class MemcachedBagOStuff extends MediumSpecificBagOStuff {
function __construct( array $params ) {
parent::__construct( $params );
*/
class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
/** @var Memcached */
- protected $client;
+ protected $syncClient;
+ /** @var Memcached|null */
+ protected $asyncClient;
+
+ /** @var bool Whether the non-buffering client is locked from use */
+ protected $syncClientIsBuffering = false;
+ /** @var bool Whether the non-buffering client should be flushed before use */
+ protected $hasUnflushedChanges = false;
+
+ /** @var array Memcached options */
+ private static $OPTS_SYNC_WRITES = [
+ Memcached::OPT_NO_BLOCK => false, // async I/O (using TCP buffers)
+ Memcached::OPT_BUFFER_WRITES => false // libmemcached buffers
+ ];
+ /** @var array Memcached options */
+ private static $OPTS_ASYNC_WRITES = [
+ Memcached::OPT_NO_BLOCK => true, // async I/O (using TCP buffers)
+ Memcached::OPT_BUFFER_WRITES => true // libmemcached buffers
+ ];
/**
* Available parameters are:
// The Memcached object is essentially shared for each pool ID.
// We can only reuse a pool ID if we keep the config consistent.
$connectionPoolId = md5( serialize( $params ) );
- $client = new Memcached( $connectionPoolId );
- $this->initializeClient( $client, $params );
+ $syncClient = new Memcached( "$connectionPoolId-sync" );
+ // Avoid clobbering the main thread-shared Memcached instance
+ $asyncClient = new Memcached( "$connectionPoolId-async" );
} else {
- $client = new Memcached;
- $this->initializeClient( $client, $params );
+ $syncClient = new Memcached();
+ $asyncClient = null;
}
- $this->client = $client;
+ $this->initializeClient( $syncClient, $params, self::$OPTS_SYNC_WRITES );
+ if ( $asyncClient ) {
+ $this->initializeClient( $asyncClient, $params, self::$OPTS_ASYNC_WRITES );
+ }
+ // Set the main client and any dedicated one for buffered writes
+ $this->syncClient = $syncClient;
+ $this->asyncClient = $asyncClient;
// The compression threshold is an undocumented php.ini option for some
// reason. There's probably not much harm in setting it globally, for
// compatibility with the settings for the PHP client.
*
* @param Memcached $client
* @param array $params
+ * @param array $options Base options for Memcached::setOptions()
* @throws RuntimeException
*/
- private function initializeClient( Memcached $client, array $params ) {
+ private function initializeClient( Memcached $client, array $params, array $options ) {
if ( $client->getServerList() ) {
$this->logger->debug( __METHOD__ . ": pre-initialized client instance." );
$this->logger->debug( __METHOD__ . ": initializing new client instance." );
- $options = [
+ $options += [
+ Memcached::OPT_NO_BLOCK => false,
+ Memcached::OPT_BUFFER_WRITES => false,
// Network protocol (ASCII or binary)
Memcached::OPT_BINARY_PROTOCOL => $params['use_binary_protocol'],
// Set various network timeouts
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$this->debug( "get($key)" );
+
+ $client = $this->acquireSyncClient();
if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
/** @noinspection PhpUndefinedClassConstantInspection */
$flags = Memcached::GET_EXTENDED;
- $res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags );
+ $res = $client->get( $this->validateKeyEncoding( $key ), null, $flags );
if ( is_array( $res ) ) {
$result = $res['value'];
$casToken = $res['cas'];
$casToken = null;
}
} else {
- $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
+ $result = $client->get( $this->validateKeyEncoding( $key ), null, $casToken );
}
- $result = $this->checkResult( $key, $result );
- return $result;
+
+ return $this->checkResult( $key, $result );
}
protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "set($key)" );
- $result = $this->client->set(
+
+ $client = $this->acquireSyncClient();
+ $result = $client->set(
$this->validateKeyEncoding( $key ),
$value,
$this->fixExpiry( $exptime )
);
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) {
+
+ return ( $result === false && $client->getResultCode() === Memcached::RES_NOTSTORED )
// "Not stored" is always used as the mcrouter response with AllAsyncRoute
- return true;
- }
- return $this->checkResult( $key, $result );
+ ? true
+ : $this->checkResult( $key, $result );
}
protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "cas($key)" );
- $result = $this->client->cas( $casToken, $this->validateKeyEncoding( $key ),
- $value, $this->fixExpiry( $exptime ) );
+
+ $result = $this->acquireSyncClient()->cas(
+ $casToken,
+ $this->validateKeyEncoding( $key ),
+ $value, $this->fixExpiry( $exptime )
+ );
+
return $this->checkResult( $key, $result );
}
protected function doDelete( $key, $flags = 0 ) {
$this->debug( "delete($key)" );
- $result = $this->client->delete( $this->validateKeyEncoding( $key ) );
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
+
+ $client = $this->acquireSyncClient();
+ $result = $client->delete( $this->validateKeyEncoding( $key ) );
+
+ return ( $result === false && $client->getResultCode() === Memcached::RES_NOTFOUND )
// "Not found" is counted as success in our interface
- return true;
- }
- return $this->checkResult( $key, $result );
+ ? true
+ : $this->checkResult( $key, $result );
}
public function add( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debug( "add($key)" );
- $result = $this->client->add(
+
+ $result = $this->acquireSyncClient()->add(
$this->validateKeyEncoding( $key ),
$value,
$this->fixExpiry( $exptime )
);
+
return $this->checkResult( $key, $result );
}
public function incr( $key, $value = 1 ) {
$this->debug( "incr($key)" );
- $result = $this->client->increment( $key, $value );
+
+ $result = $this->acquireSyncClient()->increment( $key, $value );
+
return $this->checkResult( $key, $result );
}
public function decr( $key, $value = 1 ) {
$this->debug( "decr($key)" );
- $result = $this->client->decrement( $key, $value );
+
+ $result = $this->acquireSyncClient()->decrement( $key, $value );
+
return $this->checkResult( $key, $result );
}
if ( $result !== false ) {
return $result;
}
- switch ( $this->client->getResultCode() ) {
+
+ $client = $this->syncClient;
+ switch ( $client->getResultCode() ) {
case Memcached::RES_SUCCESS:
break;
case Memcached::RES_DATA_EXISTS:
case Memcached::RES_NOTSTORED:
case Memcached::RES_NOTFOUND:
- $this->debug( "result: " . $this->client->getResultMessage() );
+ $this->debug( "result: " . $client->getResultMessage() );
break;
default:
- $msg = $this->client->getResultMessage();
+ $msg = $client->getResultMessage();
$logCtx = [];
if ( $key !== false ) {
- $server = $this->client->getServerByKey( $key );
+ $server = $client->getServerByKey( $key );
$logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
$logCtx['memcached-key'] = $key;
- $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
+ $msg = "Memcached error for key \"{memcached-key}\" " .
+ "on server \"{memcached-server}\": $msg";
} else {
$msg = "Memcached error: $msg";
}
protected function doGetMulti( array $keys, $flags = 0 ) {
$this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
+
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->getMulti( $keys ) ?: [];
+
+ // The PECL implementation uses "gets" which works as well as a pipeline
+ $result = $this->acquireSyncClient()->getMulti( $keys ) ?: [];
+
return $this->checkResult( false, $result );
}
protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
+
+ $exptime = $this->fixExpiry( $exptime );
foreach ( array_keys( $data ) as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
+
+ // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
+ // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852
+ if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+ $client = $this->acquireAsyncClient();
+ $result = $client->setMulti( $data, $exptime );
+ $this->releaseAsyncClient( $client );
+ } else {
+ $result = $this->acquireSyncClient()->setMulti( $data, $exptime );
+ }
+
return $this->checkResult( false, $result );
}
protected function doDeleteMulti( array $keys, $flags = 0 ) {
$this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
+
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
- $result = $this->client->deleteMulti( $keys ) ?: [];
- $ok = true;
- foreach ( $result as $code ) {
+
+ // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
+ // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852
+ if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+ $client = $this->acquireAsyncClient();
+ $resultArray = $client->deleteMulti( $keys ) ?: [];
+ $this->releaseAsyncClient( $client );
+ } else {
+ $resultArray = $this->acquireSyncClient()->deleteMulti( $keys ) ?: [];
+ }
+
+ $result = true;
+ foreach ( $resultArray as $code ) {
if ( !in_array( $code, [ true, Memcached::RES_NOTFOUND ], true ) ) {
// "Not found" is counted as success in our interface
- $ok = false;
+ $result = false;
}
}
- return $this->checkResult( false, $ok );
+
+ return $this->checkResult( false, $result );
}
protected function doChangeTTL( $key, $exptime, $flags ) {
$this->debug( "touch($key)" );
- $result = $this->client->touch( $key, $exptime );
+
+ $result = $this->acquireSyncClient()->touch( $key, $this->fixExpiry( $exptime ) );
+
return $this->checkResult( $key, $result );
}
return $value;
}
- $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+ $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER );
if ( $serializer === Memcached::SERIALIZER_PHP ) {
return serialize( $value );
} elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
return (int)$value;
}
- $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
+ $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER );
if ( $serializer === Memcached::SERIALIZER_PHP ) {
return unserialize( $value );
} elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
throw new UnexpectedValueException( __METHOD__ . ": got serializer '$serializer'." );
}
+
+ /**
+ * @return Memcached
+ */
+ private function acquireSyncClient() {
+ if ( $this->syncClientIsBuffering ) {
+ throw new RuntimeException( "The main (unbuffered I/O) client is locked" );
+ }
+
+ if ( $this->hasUnflushedChanges ) {
+ // Force a synchronous flush of async writes so that their changes are visible
+ $this->syncClient->fetch();
+ if ( $this->asyncClient ) {
+ $this->asyncClient->fetch();
+ }
+ $this->hasUnflushedChanges = false;
+ }
+
+ return $this->syncClient;
+ }
+
+ /**
+ * @return Memcached
+ */
+ private function acquireAsyncClient() {
+ if ( $this->asyncClient ) {
+ return $this->asyncClient; // dedicated buffering instance
+ }
+
+ // Modify the main instance to temporarily buffer writes
+ $this->syncClientIsBuffering = true;
+ $this->syncClient->setOptions( self::$OPTS_ASYNC_WRITES );
+
+ return $this->syncClient;
+ }
+
+ /**
+ * @param Memcached $client
+ */
+ private function releaseAsyncClient( $client ) {
+ $this->hasUnflushedChanges = true;
+
+ if ( !$this->asyncClient ) {
+ // This is the main instance; make it stop buffering writes again
+ $client->setOptions( self::$OPTS_SYNC_WRITES );
+ $this->syncClientIsBuffering = false;
+ }
+ }
}
$this->client->set_servers( $params['servers'] );
}
- public function setDebug( $debug ) {
- $this->client->set_debug( $debug );
+ public function setDebug( $enabled ) {
+ parent::debug( $enabled );
+ $this->client->set_debug( $enabled );
}
protected function doGet( $key, $flags = 0, &$casToken = null ) {
/** @var int[] List of all backing cache indexes */
protected $cacheIndexes = [];
- const UPGRADE_TTL = 3600; // TTL when a key is copied to a higher cache tier
+ /** @var int TTL when a key is copied to a higher cache tier */
+ private static $UPGRADE_TTL = 3600;
/**
* $params include:
$this->cacheIndexes = array_keys( $this->caches );
}
- public function setDebug( $debug ) {
+ public function setDebug( $enabled ) {
+ parent::setDebug( $enabled );
foreach ( $this->caches as $cache ) {
- $cache->setDebug( $debug );
+ $cache->setDebug( $enabled );
}
}
$this->asyncWrites,
'set',
// @TODO: consider using self::WRITE_ALLOW_SEGMENTS here?
- [ $key, $value, self::UPGRADE_TTL ]
+ [ $key, $value, self::$UPGRADE_TTL ]
);
}
return $this->caches[0]->makeGlobalKey( ...func_get_args() );
}
- protected function doGet( $key, $flags = 0, &$casToken = null ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ public function addBusyCallback( callable $workCallback ) {
+ $this->caches[0]->addBusyCallback( $workCallback );
}
- protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doDelete( $key, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doChangeTTL( $key, $exptime, $flags ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doGetMulti( array $keys, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doDeleteMulti( array $keys, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function serialize( $value ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function unserialize( $blob ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ public function setMockTime( &$time ) {
+ parent::setMockTime( $time );
+ foreach ( $this->caches as $cache ) {
+ $cache->setMockTime( $time );
+ $cache->setMockTime( $time );
+ }
}
}
* $wgSessionCacheType = 'sessions';
* @endcode
*/
-class RESTBagOStuff extends BagOStuff {
+class RESTBagOStuff extends MediumSpecificBagOStuff {
/**
* Default connection timeout in seconds. The kernel retransmits the SYN
* packet after 1 second, so 1.2 seconds allows for 1 retransmit without
* @ingroup Cache
* @ingroup Redis
*/
-class RedisBagOStuff extends BagOStuff {
+class RedisBagOStuff extends MediumSpecificBagOStuff {
/** @var RedisConnectionPool */
protected $redisPool;
/** @var array List of server names */
}
try {
- $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();
- }
- } catch ( RedisException $e ) {
- try {
- $conn->unwatch(); // sanity
- } catch ( RedisException $ex ) {
- // already errored
- }
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'incr', $key, $conn->getServer(), $result );
-
- return $result;
- }
-
- public function incrWithInit( $key, $exptime, $value = 1, $init = 1 ) {
- $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 {$conn->getServer()} failed" );
- } else {
- $result = end( $batchResult );
+ if ( !$conn->exists( $key ) ) {
+ return false;
}
+ // @FIXME: on races, the key may have a 0 TTL
+ $result = $conn->incrBy( $key, $value );
} catch ( RedisException $e ) {
$result = false;
$this->handleException( $conn, $e );
$this->attrMap = $this->mergeFlagMaps( [ $this->readStore, $this->writeStore ] );
}
- public function setDebug( $debug ) {
- $this->writeStore->setDebug( $debug );
- $this->readStore->setDebug( $debug );
+ public function setDebug( $enabled ) {
+ parent::setDebug( $enabled );
+ $this->writeStore->setDebug( $enabled );
+ $this->readStore->setDebug( $enabled );
}
public function get( $key, $flags = 0 ) {
return $this->writeStore->makeGlobalKey( ...func_get_args() );
}
- protected function doGet( $key, $flags = 0, &$casToken = null ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ public function addBusyCallback( callable $workCallback ) {
+ $this->writeStore->addBusyCallback( $workCallback );
}
- protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doDelete( $key, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doChangeTTL( $key, $exptime, $flags ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doGetMulti( array $keys, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function doDeleteMulti( array $keys, $flags = 0 ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function serialize( $value ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
- }
-
- protected function unserialize( $blob ) {
- throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ public function setMockTime( &$time ) {
+ parent::setMockTime( $time );
+ $this->writeStore->setMockTime( $time );
+ $this->readStore->setMockTime( $time );
}
}
protected $cache;
/** @var MapCacheLRU[] Map of group PHP instance caches */
protected $processCaches = [];
+ /** @var LoggerInterface */
+ protected $logger;
+ /** @var StatsdDataFactoryInterface */
+ protected $stats;
+ /** @var callable|null Function that takes a WAN cache callback and runs it later */
+ protected $asyncHandler;
+
/** @bar bool Whether to use mcrouter key prefixing for routing */
protected $mcrouterAware;
/** @var string Physical region for mcrouter use */
protected $region;
/** @var string Cache cluster name for mcrouter use */
protected $cluster;
- /** @var LoggerInterface */
- protected $logger;
- /** @var StatsdDataFactoryInterface */
- protected $stats;
/** @var bool Whether to use "interim" caching while keys are tombstoned */
protected $useInterimHoldOffCaching = true;
- /** @var callable|null Function that takes a WAN cache callback and runs it later */
- protected $asyncHandler;
/** @var float Unix timestamp of the oldest possible valid values */
protected $epoch;
/** @var string Stable secret used for hasing long strings into key components */
/** @var float|null */
private $wallClockOverride;
- /** Max time expected to pass between delete() and DB commit finishing */
+ /** @var int Max expected seconds to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
- /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
+ /** @var int Max expected seconds of combined lag from replication and view snapshots */
const MAX_READ_LAG = 7;
- /** Seconds to tombstone keys on delete() */
- const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1
-
- /** Seconds to keep dependency purge keys around */
- const CHECK_KEY_TTL = self::TTL_YEAR;
- /** Seconds to keep interim value keys for tombstoned keys around */
- const INTERIM_KEY_TTL = 1;
-
- /** Seconds to keep lock keys around */
- const LOCK_TTL = 10;
- /** Seconds to no-op key set() calls to avoid large blob I/O stampedes */
- const COOLOFF_TTL = 1;
- /** Default remaining TTL at which to consider pre-emptive regeneration */
+ /** @var int Seconds to tombstone keys on delete() and treat as volatile after invalidation */
+ const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
+
+ /** @var int Idiom for getWithSetCallback() meaning "do not store the callback result" */
+ const TTL_UNCACHEABLE = -1;
+
+ /** @var int Consider regeneration if the key will expire within this many seconds */
const LOW_TTL = 30;
- /** Max TTL to store keys when a data sourced is lagged */
+ /** @var int Max TTL, in seconds, to store keys when a data sourced is lagged */
const TTL_LAGGED = 30;
- /** Never consider performing "popularity" refreshes until a key reaches this age */
- const AGE_NEW = 60;
- /** The time length of the "popularity" refresh window for hot keys */
+ /** @var int Expected time-till-refresh, in seconds, if the key is accessed once per second */
const HOT_TTR = 900;
- /** Hits/second for a refresh to be expected within the "popularity" window */
- const HIT_RATE_HIGH = 1;
- /** Seconds to ramp up to the "popularity" refresh chance after a key is no longer new */
- const RAMPUP_TTL = 30;
+ /** @var int Minimum key age, in seconds, for expected time-till-refresh to be considered */
+ const AGE_NEW = 60;
- /** Idiom for getWithSetCallback() meaning "do not store the callback result" */
- const TTL_UNCACHEABLE = -1;
- /** Idiom for getWithSetCallback() meaning "no regeneration mutex based on key hotness" */
+ /** @var int Idiom for getWithSetCallback() meaning "no cache stampede mutex required" */
const TSE_NONE = -1;
- /** Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */
+
+ /** @var int Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */
const STALE_TTL_NONE = 0;
- /** Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period" */
+ /** @var int Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period" */
const GRACE_TTL_NONE = 0;
- /** Idiom for delete()/touchCheckKey() meaning "no hold-off period for cache writes" */
- const HOLDOFF_NONE = 0;
+ /** @var int Idiom for delete()/touchCheckKey() meaning "no hold-off period" */
+ const HOLDOFF_TTL_NONE = 0;
+ /** @var int Alias for HOLDOFF_TTL_NONE (b/c) (deprecated since 1.34) */
+ const HOLDOFF_NONE = self::HOLDOFF_TTL_NONE;
- /** Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */
+ /** @var float 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" */
- 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 string Default process cache name and max key count */
+ const PC_PRIMARY = 'primary:1000';
- /** @var int Seconds needed for value generation considered slow */
- const GENERATION_SLOW_SEC = 3;
-
- /** Parameter to get()/getMulti() to return extra information by reference */
+ /** @var int Idion for get()/getMulti() to return extra information by reference */
const PASS_BY_REF = -1;
- /** Cache format version number */
- const VERSION = 1;
-
- 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 timestamp
- const FLD_FLAGS = 4; // key to the flags bitfield (reserved number)
- 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 TIME_KEY_PREFIX = 'WANCache:t:';
- const MUTEX_KEY_PREFIX = 'WANCache:m:';
- const COOLOFF_KEY_PREFIX = 'WANCache:c:';
-
- const PURGE_VAL_PREFIX = 'PURGED:';
-
- const PC_PRIMARY = 'primary:1000'; // process cache name and max key count
+ /** @var int Seconds to keep dependency purge keys around */
+ private static $CHECK_KEY_TTL = self::TTL_YEAR;
+ /** @var int Seconds to keep interim value keys for tombstoned keys around */
+ private static $INTERIM_KEY_TTL = 1;
+
+ /** @var int Seconds to keep lock keys around */
+ private static $LOCK_TTL = 10;
+ /** @var int Seconds to no-op key set() calls to avoid large blob I/O stampedes */
+ private static $COOLOFF_TTL = 1;
+ /** @var int Seconds to ramp up the chance of regeneration due to expected time-till-refresh */
+ private static $RAMPUP_TTL = 30;
+
+ /** @var float Tiny negative float to use when CTL comes up >= 0 due to clock skew */
+ private static $TINY_NEGATIVE = -0.000001;
+ /** @var float Tiny positive float to use when using "minTime" to assert an inequality */
+ private static $TINY_POSTIVE = 0.000001;
+
+ /** @var int Milliseconds of key fetch/validate/regenerate delay prone to set() stampedes */
+ private static $SET_DELAY_HIGH_MS = 50;
+ /** @var int Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */
+ private static $RECENT_SET_LOW_MS = 50;
+ /** @var int Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */
+ private static $RECENT_SET_HIGH_MS = 100;
+
+ /** @var int Consider value generation slow if it takes more than this many seconds */
+ private static $GENERATION_SLOW_SEC = 3;
+
+ /** @var int Key to the tombstone entry timestamp */
+ private static $PURGE_TIME = 0;
+ /** @var int Key to the tombstone entry hold-off TTL */
+ private static $PURGE_HOLDOFF = 1;
+
+ /** @var int Cache format version number */
+ private static $VERSION = 1;
+
+ /** @var int Key to WAN cache version number */
+ private static $FLD_FORMAT_VERSION = 0;
+ /** @var int Key to the cached value */
+ private static $FLD_VALUE = 1;
+ /** @var int Key to the original TTL */
+ private static $FLD_TTL = 2;
+ /** @var int Key to the cache timestamp */
+ private static $FLD_TIME = 3;
+ /** @var int Key to the flags bit field (reserved number) */
+ private static /** @noinspection PhpUnusedPrivateFieldInspection */ $FLD_FLAGS = 4;
+ /** @var int Key to collection cache version number */
+ private static $FLD_VALUE_VERSION = 5;
+ /** @var int Key to how long it took to generate the value */
+ private static $FLD_GENERATION_TIME = 6;
+
+ private static $VALUE_KEY_PREFIX = 'WANCache:v:';
+ private static $INTERIM_KEY_PREFIX = 'WANCache:i:';
+ private static $TIME_KEY_PREFIX = 'WANCache:t:';
+ private static $MUTEX_KEY_PREFIX = 'WANCache:m:';
+ private static $COOLOFF_KEY_PREFIX = 'WANCache:c:';
+
+ private static $PURGE_VAL_PREFIX = 'PURGED:';
/**
* @param array $params
$this->region = $params['region'] ?? 'main';
$this->cluster = $params['cluster'] ?? 'wan-main';
$this->mcrouterAware = !empty( $params['mcrouterAware'] );
- $this->epoch = $params['epoch'] ?? self::EPOCH_UNIX_ONE_SECOND;
+ $this->epoch = $params['epoch'] ?? 0;
$this->secret = $params['secret'] ?? (string)$this->epoch;
$this->setLogger( $params['logger'] ?? new NullLogger() );
$curTTLs = [];
$infoByKey = [];
- $vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
- $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
+ $vPrefixLen = strlen( self::$VALUE_KEY_PREFIX );
+ $valueKeys = self::prefixCacheKeys( $keys, self::$VALUE_KEY_PREFIX );
$checkKeysForAll = [];
$checkKeysByKey = [];
$checkKeysFlat = [];
foreach ( $checkKeys as $i => $checkKeyGroup ) {
- $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::TIME_KEY_PREFIX );
+ $prefixed = self::prefixCacheKeys( (array)$checkKeyGroup, self::$TIME_KEY_PREFIX );
$checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
// Are these check keys for a specific cache key, or for all keys being fetched?
if ( is_int( $i ) ) {
$lastCKPurge = null; // timestamp of the highest check key
foreach ( $purgeValues as $purge ) {
- $lastCKPurge = max( $purge[self::PURGE_TIME], $lastCKPurge );
- $safeTimestamp = $purge[self::PURGE_TIME] + $purge[self::PURGE_HOLDOFF];
+ $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::PURGE_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
$keyInfo['curTTL'] = min( $keyInfo['curTTL'], $ago );
}
if ( $purge === false ) {
// Key is not set or malformed; regenerate
$newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
- $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
+ $this->cache->add( $timeKey, $newVal, self::$CHECK_KEY_TTL );
$purge = $this->parsePurgeValue( $newVal );
}
$purgeValues[] = $purge;
$storeTTL = $ttl + $staleTTL;
if ( $creating ) {
- $ok = $this->cache->add( self::VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
+ $ok = $this->cache->add( self::$VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
} else {
$ok = $this->cache->merge(
- self::VALUE_KEY_PREFIX . $key,
+ self::$VALUE_KEY_PREFIX . $key,
function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
// A string value means that it is a tombstone; do nothing in that case
return ( is_string( $cWrapped ) ) ? false : $wrapped;
*
* The $ttl parameter can be used when purging values that have not actually changed
* recently. For example, a cleanup script to purge cache entries does not really need
- * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge.
+ * a hold-off period, so it can use HOLDOFF_TTL_NONE. Likewise for user-requested purge.
* Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback().
*
* If called twice on the same key, then the last hold-off TTL takes precedence. For
final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
if ( $ttl <= 0 ) {
// Publish the purge to all datacenters
- $ok = $this->relayDelete( self::VALUE_KEY_PREFIX . $key );
+ $ok = $this->relayDelete( self::$VALUE_KEY_PREFIX . $key );
} else {
// Publish the purge to all datacenters
- $ok = $this->relayPurge( self::VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_NONE );
+ $ok = $this->relayPurge( self::$VALUE_KEY_PREFIX . $key, $ttl, self::HOLDOFF_TTL_NONE );
}
$kClass = $this->determineKeyClassForStats( $key );
final public function getMultiCheckKeyTime( array $keys ) {
$rawKeys = [];
foreach ( $keys as $key ) {
- $rawKeys[$key] = self::TIME_KEY_PREFIX . $key;
+ $rawKeys[$key] = self::$TIME_KEY_PREFIX . $key;
}
$rawValues = $this->cache->getMulti( $rawKeys );
foreach ( $rawKeys as $key => $rawKey ) {
$purge = $this->parsePurgeValue( $rawValues[$rawKey] );
if ( $purge !== false ) {
- $time = $purge[self::PURGE_TIME];
+ $time = $purge[self::$PURGE_TIME];
} else {
// Casting assures identical floats for the next getCheckKeyTime() calls
$now = (string)$this->getCurrentTime();
$this->cache->add(
$rawKey,
$this->makePurgeValue( $now, self::HOLDOFF_TTL ),
- self::CHECK_KEY_TTL
+ self::$CHECK_KEY_TTL
);
$time = (float)$now;
}
* @see WANObjectCache::resetCheckKey()
*
* @param string $key Cache key
- * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant
+ * @param int $holdoff HOLDOFF_TTL or HOLDOFF_TTL_NONE constant
* @return bool True if the item was purged or not found, false on failure
*/
final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
// Publish the purge to all datacenters
- $ok = $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
+ $ok = $this->relayPurge( self::$TIME_KEY_PREFIX . $key, self::$CHECK_KEY_TTL, $holdoff );
$kClass = $this->determineKeyClassForStats( $key );
$this->stats->increment( "wanobjectcache.$kClass.ck_touch." . ( $ok ? 'ok' : 'error' ) );
*/
final public function resetCheckKey( $key ) {
// Publish the purge to all datacenters
- $ok = $this->relayDelete( self::TIME_KEY_PREFIX . $key );
+ $ok = $this->relayDelete( self::$TIME_KEY_PREFIX . $key );
$kClass = $this->determineKeyClassForStats( $key );
$this->stats->increment( "wanobjectcache.$kClass.ck_reset." . ( $ok ? 'ok' : 'error' ) );
// This avoids stampedes on eviction or preemptive regeneration taking too long.
( $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,
- $version,
- $curInfo['asOf']
- ];
- }
+ // If a regeneration lock is required, threads that do not get the lock will try to use
+ // the stale value, the interim value, or the $busyValue placeholder, in that order. If
+ // none of those are set then all threads will bypass the lock and regenerate the value.
+ $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key );
+ if ( $useRegenerationLock && !$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,
+ $version,
+ $curInfo['asOf']
+ ];
}
- } else {
- $hasLock = false;
}
// Generate the new value given any prior value with a matching version
}
}
- if ( $hasLock ) {
- $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$initialTime - 60 );
- }
+ $this->yieldStampedeLock( $key, $hasLock );
$miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
$this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
return [ $value, $version, $curInfo['asOf'] ];
}
+ /**
+ * @param string $key
+ * @return bool Success
+ */
+ private function claimStampedeLock( $key ) {
+ // Note that locking is not bypassed due to I/O errors; this avoids stampedes
+ return $this->cache->add( self::$MUTEX_KEY_PREFIX . $key, 1, self::$LOCK_TTL );
+ }
+
+ /**
+ * @param string $key
+ * @param bool $hasLock
+ */
+ private function yieldStampedeLock( $key, $hasLock ) {
+ if ( $hasLock ) {
+ // The backend might be a mcrouter proxy set to broadcast DELETE to *all* the local
+ // datacenter cache servers via OperationSelectorRoute (for increased consistency).
+ // Since that would be excessive for these locks, use TOUCH to expire the key.
+ $this->cache->changeTTL( self::$MUTEX_KEY_PREFIX . $key, $this->getCurrentTime() - 60 );
+ }
+ }
+
/**
* @param float $age Age of volatile/interim key in seconds
* @return bool Whether the age of a volatile value is negligible
*/
private function isVolatileValueAgeNegligible( $age ) {
- return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
+ return ( $age < mt_rand( self::$RECENT_SET_LOW_MS, self::$RECENT_SET_HIGH_MS ) / 1e3 );
}
/**
// consistent hashing).
if ( $lockTSE < 0 || $hasLock ) {
return true; // either not a priori hot or thread has the lock
- } elseif ( $elapsed <= self::SET_DELAY_HIGH_MS * 1e3 ) {
+ } elseif ( $elapsed <= self::$SET_DELAY_HIGH_MS * 1e3 ) {
return true; // not enough time for threads to pile up
}
$this->cache->clearLastError();
if (
- !$this->cache->add( self::COOLOFF_KEY_PREFIX . $key, 1, self::COOLOFF_TTL ) &&
+ !$this->cache->add( self::$COOLOFF_KEY_PREFIX . $key, 1, self::$COOLOFF_TTL ) &&
// Don't treat failures due to I/O errors as the key being in cooloff
$this->cache->getLastError() === BagOStuff::ERR_NONE
) {
$touched = $touchedCallback( $value );
if ( $touched !== null && $touched >= $curInfo['asOf'] ) {
- $curTTL = min( $curTTL, self::TINY_NEGATIVE, $curInfo['asOf'] - $touched );
+ $curTTL = min( $curTTL, self::$TINY_NEGATIVE, $curInfo['asOf'] - $touched );
}
return [ $curTTL, max( $curInfo['tombAsOf'], $curInfo['lastCKPurge'], $touched ) ];
$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, $keyInfo ) = $this->unwrap( $wrapped, $now );
if ( $this->isValid( $value, $keyInfo['asOf'], $minAsOf ) ) {
* @param float $walltime How long it took to generate the value in seconds
*/
private function setInterimValue( $key, $value, $ttl, $version, $walltime ) {
- $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
+ $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,
+ self::$INTERIM_KEY_PREFIX . $key,
function () use ( $wrapped ) {
return $wrapped;
},
*/
final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
$minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
- $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key );
- if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
+ $wrapped = $this->cache->get( self::$VALUE_KEY_PREFIX . $key );
+ if ( is_array( $wrapped ) && $wrapped[self::$FLD_TIME] < $minAsOf ) {
$isStale = true;
$this->logger->warning( "Reaping stale value key '$key'." );
$ttlReap = self::HOLDOFF_TTL; // avoids races with tombstone creation
- $ok = $this->cache->changeTTL( self::VALUE_KEY_PREFIX . $key, $ttlReap );
+ $ok = $this->cache->changeTTL( self::$VALUE_KEY_PREFIX . $key, $ttlReap );
if ( !$ok ) {
$this->logger->error( "Could not complete reap of key '$key'." );
}
* @since 1.28
*/
final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
- $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
- if ( $purge && $purge[self::PURGE_TIME] < $purgeTimestamp ) {
+ $purge = $this->parsePurgeValue( $this->cache->get( self::$TIME_KEY_PREFIX . $key ) );
+ 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 );
+ $ok = $this->cache->changeTTL( self::$TIME_KEY_PREFIX . $key, self::TTL_SECOND );
if ( !$ok ) {
$this->logger->error( "Could not complete reap of check key '$key'." );
}
// Wildcards select all matching routes, e.g. the WAN cluster on all DCs
$ok = $this->cache->set(
"/*/{$this->cluster}/{$key}",
- $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
+ $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ),
$ttl
);
} else {
// This handles the mcrouter and the single-DC case
$ok = $this->cache->set(
$key,
- $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
+ $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ),
$ttl
);
}
return false;
}
+ $popularHitsPerSec = 1;
// Lifecycle is: new, ramp-up refresh chance, full refresh chance.
- // Note that the "expected # of refreshes" for the ramp-up time range is half of what it
- // would be if P(refresh) was at its full value during that time range.
- $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
+ // Note that the "expected # of refreshes" for the ramp-up time range is half
+ // of what it would be if P(refresh) was at its full value during that time range.
+ $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::$RAMPUP_TTL / 2, 1 );
// P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
- // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1
+ // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
// P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
- $chance = 1 / ( self::HIT_RATE_HIGH * $refreshWindowSec );
+ $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
// Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
- $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
+ $chance *= ( $timeOld <= self::$RAMPUP_TTL ) ? $timeOld / self::$RAMPUP_TTL : 1;
return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
}
*/
protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
// Avoid reading any key not generated after the latest delete() or touch
- $safeMinAsOf = max( $minAsOf, $purgeTime + self::TINY_POSTIVE );
+ $safeMinAsOf = max( $minAsOf, $purgeTime + self::$TINY_POSTIVE );
if ( $value === false ) {
return false;
// 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
+ 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;
+ $wrapped[self::$FLD_VALUE_VERSION] = $version;
}
- if ( $walltime >= self::GENERATION_SLOW_SEC ) {
- $wrapped[self::FLD_GENERATION_TIME] = $walltime;
+ if ( $walltime >= self::$GENERATION_SLOW_SEC ) {
+ $wrapped[self::$FLD_GENERATION_TIME] = $walltime;
}
return $wrapped;
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
+ ( $wrapped[self::$FLD_FORMAT_VERSION] ?? null ) === self::$VERSION &&
+ $wrapped[self::$FLD_TIME] >= $this->epoch
) {
- if ( $wrapped[self::FLD_TTL] > 0 ) {
+ 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 );
+ $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];
+ $value = $wrapped[self::$FLD_VALUE];
+ $info['version'] = $wrapped[self::$FLD_VALUE_VERSION] ?? null;
+ $info['asOf'] = $wrapped[self::$FLD_TIME];
$info['curTTL'] = $curTTL;
}
} else {
$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];
+ $info['curTTL'] = min( $purge[self::$PURGE_TIME] - $now, self::$TINY_NEGATIVE );
+ $info['tombAsOf'] = $purge[self::$PURGE_TIME];
}
}
}
$segments = explode( ':', $value, 3 );
- if ( !isset( $segments[0] ) || !isset( $segments[1] )
- || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
+ if (
+ !isset( $segments[0] ) ||
+ !isset( $segments[1] ) ||
+ "{$segments[0]}:" !== self::$PURGE_VAL_PREFIX
) {
return false;
}
}
return [
- self::PURGE_TIME => (float)$segments[1],
- self::PURGE_HOLDOFF => (int)$segments[2],
+ self::$PURGE_TIME => (float)$segments[1],
+ self::$PURGE_HOLDOFF => (int)$segments[2],
];
}
* @return string Wrapped purge value
*/
private function makePurgeValue( $timestamp, $holdoff ) {
- return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
+ return self::$PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
}
/**
$keysWarmUp = [];
// Get all the value keys to fetch...
foreach ( $keys as $key ) {
- $keysWarmUp[] = self::VALUE_KEY_PREFIX . $key;
+ $keysWarmUp[] = self::$VALUE_KEY_PREFIX . $key;
}
// Get all the check keys to fetch...
foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
if ( is_int( $i ) ) {
// Single check key that applies to all value keys
- $keysWarmUp[] = self::TIME_KEY_PREFIX . $checkKeyOrKeys;
+ $keysWarmUp[] = self::$TIME_KEY_PREFIX . $checkKeyOrKeys;
} else {
// List of check keys that apply to value key $i
$keysWarmUp = array_merge(
$keysWarmUp,
- self::prefixCacheKeys( $checkKeyOrKeys, self::TIME_KEY_PREFIX )
+ self::prefixCacheKeys( $checkKeyOrKeys, self::$TIME_KEY_PREFIX )
);
}
}
*
* @ingroup Cache
*/
-class WinCacheBagOStuff extends BagOStuff {
+class WinCacheBagOStuff extends MediumSpecificBagOStuff {
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$casToken = null;
*
* @ingroup Cache
*/
-class SqlBagOStuff extends BagOStuff {
+class SqlBagOStuff extends MediumSpecificBagOStuff {
/** @var array[] (server index => server config) */
protected $serverInfos;
/** @var string[] (server index => tag/host name) */
protected $tableName = 'objectcache';
/** @var bool */
protected $replicaOnly = false;
- /** @var int */
- protected $syncTimeout = 3;
/** @var LoadBalancer|null */
protected $separateMainLB;
if ( isset( $params['shards'] ) ) {
$this->shards = intval( $params['shards'] );
}
- if ( isset( $params['syncTimeout'] ) ) {
- $this->syncTimeout = $params['syncTimeout'];
- }
// Backwards-compatibility for < 1.34
$this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false );
}
// - This global check key invalidates message blobs for all modules for all wikis
// in cache contexts (e.g. languages, skins). Setting a hold-off on this key could
// cause a cache stampede since no values would be stored for several seconds.
- $cache->touchCheckKey( $cache->makeGlobalKey( __CLASS__ ), $cache::HOLDOFF_NONE );
+ $cache->touchCheckKey( $cache->makeGlobalKey( __CLASS__ ), $cache::HOLDOFF_TTL_NONE );
}
/**
throw new MWException( __METHOD__ . ": invalid versionCallback for file" .
" \"{$fileInfo['name']}\" in module \"{$this->getName()}\"" );
}
- $expanded['definitionSummary'] = ( $fileInfo['versionCallback'] )( $context );
+ $expanded['definitionSummary'] =
+ ( $fileInfo['versionCallback'] )( $context, $this->getConfig() );
// Don't invoke 'callback' here as it may be expensive (T223260).
$expanded['callback'] = $fileInfo['callback'];
} else {
- $expanded['content'] = ( $fileInfo['callback'] )( $context );
+ $expanded['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
}
} elseif ( isset( $fileInfo['config'] ) ) {
if ( $type !== 'data' ) {
$fileInfo['content'] = $content;
unset( $fileInfo['filePath'] );
} elseif ( isset( $fileInfo['callback'] ) ) {
- $fileInfo['content'] = ( $fileInfo['callback'] )( $context );
+ $fileInfo['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
unset( $fileInfo['callback'] );
}
// accidentally returning it so best check and fix
$status = Status::wrap( $status );
} elseif ( is_string( $status ) ) {
- $status = Status::newFatal( new RawMessage( '$1', $status ) );
+ $status = Status::newFatal( new RawMessage( '$1', [ $status ] ) );
} elseif ( is_array( $status ) ) {
if ( is_string( reset( $status ) ) ) {
$status = Status::newFatal( ...$status );
*/
class SpecialMute extends FormSpecialPage {
+ const PAGE_NAME = 'Mute';
+
/** @var User */
private $target;
$this->centralIdLookup = CentralIdLookup::factory();
- parent::__construct( 'Mute', '', false );
+ parent::__construct( self::PAGE_NAME, '', false );
}
/**
parent::execute( $par );
$out = $this->getOutput();
- $out->addModules( 'mediawiki.special.pageLanguage' );
+ $out->addModules( 'mediawiki.misc-authed-ooui' );
}
/**
* @return bool
*/
public function onSubmit( array $data, HTMLForm $form = null ) {
- if ( !empty( $data['MuteEmail'] ) ) {
- $this->muteEmailsFromTarget();
- } else {
- $this->unmuteEmailsFromTarget();
+ foreach ( $data as $userOption => $value ) {
+ if ( $value ) {
+ $this->muteTarget( $userOption );
+ } else {
+ $this->unmuteTarget( $userOption );
+ }
}
return true;
}
/**
- * Un-mute emails from target
+ * Un-mute target
+ *
+ * @param string $userOption up_property key that holds the blacklist
*/
- private function unmuteEmailsFromTarget() {
- $blacklist = $this->getBlacklist();
+ private function unmuteTarget( $userOption ) {
+ $blacklist = $this->getBlacklist( $userOption );
$key = array_search( $this->targetCentralId, $blacklist );
if ( $key !== false ) {
$blacklist = implode( "\n", $blacklist );
$user = $this->getUser();
- $user->setOption( 'email-blacklist', $blacklist );
+ $user->setOption( $userOption, $blacklist );
$user->saveSettings();
}
}
/**
- * Mute emails from target
+ * Mute target
+ * @param string $userOption up_property key that holds the blacklist
*/
- private function muteEmailsFromTarget() {
+ private function muteTarget( $userOption ) {
// avoid duplicates just in case
- if ( !$this->isTargetBlacklisted() ) {
- $blacklist = $this->getBlacklist();
+ if ( !$this->isTargetBlacklisted( $userOption ) ) {
+ $blacklist = $this->getBlacklist( $userOption );
$blacklist[] = $this->targetCentralId;
$blacklist = implode( "\n", $blacklist );
$user = $this->getUser();
- $user->setOption( 'email-blacklist', $blacklist );
+ $user->setOption( $userOption, $blacklist );
$user->saveSettings();
}
}
/**
* @inheritDoc
*/
- protected function alterForm( HTMLForm $form ) {
+ protected function getForm() {
+ $form = parent::getForm();
$form->setId( 'mw-specialmute-form' );
$form->setHeaderText( $this->msg( 'specialmute-header', $this->target )->parse() );
$form->setSubmitTextMsg( 'specialmute-submit' );
$form->setSubmitID( 'save' );
+
+ return $form;
}
/**
* @inheritDoc
*/
protected function getFormFields() {
- if ( !$this->enableUserEmailBlacklist || !$this->enableUserEmail ) {
- throw new ErrorPageError( 'specialmute', 'specialmute-error-email-blacklist-disabled' );
+ $fields = [];
+ if (
+ $this->enableUserEmailBlacklist &&
+ $this->enableUserEmail &&
+ $this->getUser()->getEmailAuthenticationTimestamp()
+ ) {
+ $fields['email-blacklist'] = [
+ 'type' => 'check',
+ 'label-message' => 'specialmute-label-mute-email',
+ 'default' => $this->isTargetBlacklisted( 'email-blacklist' ),
+ ];
}
- if ( !$this->getUser()->getEmailAuthenticationTimestamp() ) {
- throw new ErrorPageError( 'specialmute', 'specialmute-error-email-preferences' );
- }
+ Hooks::run( 'SpecialMuteModifyFormFields', [ $this, &$fields ] );
- $fields['MuteEmail'] = [
- 'type' => 'check',
- 'label-message' => 'specialmute-label-mute-email',
- 'default' => $this->isTargetBlacklisted(),
- ];
+ if ( count( $fields ) == 0 ) {
+ throw new ErrorPageError( 'specialmute', 'specialmute-error-no-options' );
+ }
return $fields;
}
}
/**
+ * @param string $userOption
* @return bool
*/
- private function isTargetBlacklisted() {
- $blacklist = $this->getBlacklist();
- return in_array( $this->targetCentralId, $blacklist );
+ public function isTargetBlacklisted( $userOption ) {
+ $blacklist = $this->getBlacklist( $userOption );
+ return in_array( $this->targetCentralId, $blacklist, true );
}
/**
+ * @param string $userOption
* @return array
*/
- private function getBlacklist() {
- $blacklist = $this->getUser()->getOption( 'email-blacklist' );
+ private function getBlacklist( $userOption ) {
+ $blacklist = $this->getUser()->getOption( $userOption );
if ( !$blacklist ) {
return [];
}
return $result;
}
- $error = '';
- if ( !Hooks::run( 'UploadVerification',
- [ $this->mDestName, $this->mTempPath, &$error ], '1.28' )
- ) {
- return [ 'status' => self::HOOK_ABORTED, 'error' => $error ];
- }
-
return [ 'status' => self::OK ];
}
* @throws UploadStashNotLoggedInException
*/
public function stashFile( User $user = null ) {
+ wfDeprecated( __METHOD__, '1.28' );
+
return $this->doStashFile( $user );
}
return $file;
}
- /**
- * Stash a file in a temporary directory, returning a key which can be used
- * to find the file again. See stashFile().
- *
- * @deprecated since 1.28
- * @return string File key
- */
- public function stashFileGetKey() {
- wfDeprecated( __METHOD__, '1.28' );
- return $this->doStashFile()->getFileKey();
- }
-
- /**
- * alias for stashFileGetKey, for backwards compatibility
- *
- * @deprecated since 1.28
- * @return string File key
- */
- public function stashSession() {
- wfDeprecated( __METHOD__, '1.28' );
- return $this->doStashFile()->getFileKey();
- }
-
/**
* If we've modified the upload file we need to manually remove it
* on exit to clean up.
*/
public function stashFile( User $user = null ) {
wfDeprecated( __METHOD__, '1.28' );
- $this->verifyChunk();
- return parent::stashFile( $user );
- }
- /**
- * @inheritDoc
- * @throws UploadChunkVerificationException
- * @deprecated since 1.28
- */
- public function stashFileGetKey() {
- wfDeprecated( __METHOD__, '1.28' );
$this->verifyChunk();
- return parent::stashFileGetKey();
- }
-
- /**
- * @inheritDoc
- * @throws UploadChunkVerificationException
- * @deprecated since 1.28
- */
- public function stashSession() {
- wfDeprecated( __METHOD__, '1.28' );
- $this->verifyChunk();
- return parent::stashSession();
+ return parent::stashFile( $user );
}
/**
"right-editmyusercss": "Edit your own user CSS files",
"right-editmyuserjson": "Edit your own user JSON files",
"right-editmyuserjs": "Edit your own user JavaScript files",
+ "right-editmyuserjsredirect": "Edit your own user JavaScript files that are redirects",
"right-viewmywatchlist": "View your own watchlist",
"right-editmywatchlist": "Edit your own watchlist. Note some actions will still add pages even without this right.",
"right-viewmyprivateinfo": "View your own private data (e.g. email address, real name)",
"action-editmyusercss": "edit your own user CSS files",
"action-editmyuserjson": "edit your own user JSON files",
"action-editmyuserjs": "edit your own user JavaScript files",
+ "action-editmyuserjsredirect": "edit your own user JavaScript files that are redirects",
"action-viewsuppressed": "view revisions hidden from any user",
"action-hideuser": "block a username, hiding it from the public",
"action-ipblock-exempt": "bypass IP blocks, auto-blocks and range blocks",
"specialmute-success": "Your mute preferences have been updated. See all muted users in [[Special:Preferences|your preferences]].",
"specialmute-submit": "Confirm",
"specialmute-label-mute-email": "Mute emails from this user",
- "specialmute-header": "Please select your mute preferences for {{BIDI:[[User:$1]]}}.",
+ "specialmute-header": "Please select your mute preferences for user <b>{{BIDI:[[User:$1|$1]]}}</b>.",
"specialmute-error-invalid-user": "The username requested could not be found.",
- "specialmute-error-email-blacklist-disabled": "Muting users from sending you emails is not enabled.",
- "specialmute-error-email-preferences": "You must confirm your email address before you can mute a user. You may do so from [[Special:Preferences]].",
- "specialmute-email-footer": "To manage email preferences for {{BIDI:$2}} please visit <$1>.",
+ "specialmute-error-no-options": "Mute features are unavailable. This might be because: you haven't confirmed your email address or the wiki administrator has disabled email features and/or email blacklist for this wiki.",
+ "specialmute-email-footer": "To manage email preferences for user {{BIDI:$2}} please visit <$1>.",
"specialmute-login-required": "Please log in to change your mute preferences.",
"mute-preferences": "Mute preferences",
"revid": "revision $1",
"passwordpolicies-policy-passwordnotinlargeblacklist": "Password cannot be in the list of 100,000 most commonly used passwords.",
"passwordpolicies-policyflag-forcechange": "must change on login",
"passwordpolicies-policyflag-suggestchangeonlogin": "suggest change on login",
+ "mycustomjsredirectprotected": "You do not have permission to edit this JavaScript page because it is a redirect and it does not point inside your userspace.",
"easydeflate-invaliddeflate": "Content provided is not properly deflated",
"unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
"userlogout-continue": "Do you want to log out?"
"right-editsitejs": "{{doc-right|editsitejs}}",
"right-editmyusercss": "{{doc-right|editmyusercss}}\nSee also:\n* {{msg-mw|Right-editusercss}}",
"right-editmyuserjson": "{{doc-right|editmyuserjson}}\nSee also:\n* {{msg-mw|Right-edituserjson}}",
- "right-editmyuserjs": "{{doc-right|editmyuserjs}}\nSee also:\n* {{msg-mw|Right-edituserjs}}",
+ "right-editmyuserjs": "{{doc-right|editmyuserjs}}\nSee also:\n* {{msg-mw|Right-edituserjs}}\n* {{msg-mw|Right-editmyuserjsredirect}}",
+ "right-editmyuserjsredirect": "{{doc-right|editmyuserjsredirect}}\nSame as {{msg-mw|Right-editmyuserjs}} except if page is a redirect.\n\nSee also:\n* {{msg-mw|Right-edituserjs}}",
"right-viewmywatchlist": "{{doc-right|viewmywatchlist}}",
"right-editmywatchlist": "{{doc-right|editmywatchlist}}",
"right-viewmyprivateinfo": "{{doc-right|viewmyprivateinfo}}",
"action-editmyusercss": "{{doc-action|editmyusercss}}",
"action-editmyuserjson": "{{doc-action|editmyuserjson}}",
"action-editmyuserjs": "{{doc-action|editmyuserjs}}",
+ "action-editmyuserjsredirect": "{{doc-action|editmyuserjsredirect}}",
"action-viewsuppressed": "{{doc-action|viewsuppressed}}",
"action-hideuser": "{{doc-action|hideuser}}",
"action-ipblock-exempt": "{{doc-action|ipblock-exempt}}",
"specialmute-label-mute-email": "Label for the checkbox that mutes/unmutes emails from the specified user.",
"specialmute-header": "Used as header text on [[Special:Mute]]. Shown before the form with the muting options.\n* $1 - User selected for muting",
"specialmute-error-invalid-user": "Error displayed when the username cannot be found.",
- "specialmute-error-email-blacklist-disabled": "Error displayed when email blacklist is not enabled.",
- "specialmute-error-email-preferences": "Error displayed when the user has not confirmed their email address.",
+ "specialmute-error-no-options": "Error displayed when there are no options available to mute on [[Special:Mute]].",
"specialmute-email-footer": "Email footer in plain text linking to [[Special:Mute]] preselecting the sender to manage muting options.\n* $1 - Url linking to [[Special:Mute]].\n* $2 - The user sending the email.",
"specialmute-login-required": "Error displayed when a user tries to access [[Special:Mute]] before logging in.",
"mute-preferences": "Link in the sidebar to manage muting preferences for a user. It links to [[Special:Mute]] with the user in context as the subpage.",
"passwordpolicies-policy-passwordnotinlargeblacklist": "Password policy that enforces that a password is not in a list of 100,000 number of \"popular\" passwords.",
"passwordpolicies-policyflag-forcechange": "Password policy flag that enforces changing invalid passwords on login.",
"passwordpolicies-policyflag-suggestchangeonlogin": "Password policy flag that suggests changing invalid passwords on login.",
+ "mycustomjsredirectprotected": "Error message shown when user tries to edit their own JS page that is a foreign redirect without the 'mycustomjsredirectprotected' right. See also {{mw-msg|mycustomjsprotected}}.",
"easydeflate-invaliddeflate": "Error message if the content passed to easydeflate was not deflated (compressed) properly",
"unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
"userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
. " memcached server and shows a report" );
$this->addOption( 'i', 'Number of iterations', false, true );
$this->addOption( 'cache', 'Use servers from this $wgObjectCaches store', false, true );
+ $this->addOption( 'driver', 'Either "php" or "pecl"', false, true );
$this->addArg( 'server[:port]', 'Memcached server to test, with optional port', false );
}
# find out the longest server string to nicely align output later on
$maxSrvLen = $servers ? max( array_map( 'strlen', $servers ) ) : 0;
+ $type = $this->getOption( 'driver', 'php' );
+ if ( $type === 'php' ) {
+ $class = MemcachedPhpBagOStuff::class;
+ } elseif ( $type === 'pecl' ) {
+ $class = MemcachedPeclBagOStuff::class;
+ } else {
+ $this->fatalError( "Invalid driver type '$type'" );
+ }
+
foreach ( $servers as $server ) {
- $this->output(
- str_pad( $server, $maxSrvLen ),
- $server # output channel
- );
+ $this->output( str_pad( $server, $maxSrvLen ) . "\n" );
- $mcc = new MemcachedClient( [
- 'persistant' => true,
+ /** @var BagOStuff $mcc */
+ $mcc = new $class( [
+ 'servers' => [ $server ],
+ 'persistent' => true,
'timeout' => $wgMemCachedTimeout
] );
- $mcc->set_servers( [ $server ] );
- $set = 0;
- $incr = 0;
- $get = 0;
- $time_start = microtime( true );
- for ( $i = 1; $i <= $iterations; $i++ ) {
- if ( $mcc->set( "test$i", $i ) ) {
- $set++;
- }
+
+ $this->benchmarkSingleKeyOps( $mcc, $iterations );
+ $this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations );
+ $this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations );
+ }
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkSingleKeyOps( $mcc, $iterations ) {
+ $add = 0;
+ $set = 0;
+ $incr = 0;
+ $get = 0;
+ $delete = 0;
+
+ $keys = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keys[] = "test$i";
+ }
+
+ // Clear out any old values
+ $mcc->deleteMulti( $keys );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->add( $key, $i ) ) {
+ $add++;
}
- for ( $i = 1; $i <= $iterations; $i++ ) {
- if ( !is_null( $mcc->incr( "test$i", $i ) ) ) {
- $incr++;
- }
+ }
+ $addMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->set( $key, $i ) ) {
+ $set++;
+ }
+ }
+ $setMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( !is_null( $mcc->incr( $key, $i ) ) ) {
+ $incr++;
}
- for ( $i = 1; $i <= $iterations; $i++ ) {
- $value = $mcc->get( "test$i" );
- if ( $value == $i * 2 ) {
- $get++;
- }
+ }
+ $incrMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ $value = $mcc->get( $key );
+ if ( $value == $i * 2 ) {
+ $get++;
}
- $exectime = microtime( true ) - $time_start;
+ }
+ $getMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
- $this->output( " set: $set incr: $incr get: $get time: $exectime", $server );
+ $time_start = microtime( true );
+ foreach ( $keys as $key ) {
+ if ( $mcc->delete( $key ) ) {
+ $delete++;
+ }
}
+ $delMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " add: $add/$iterations {$addMs}ms " .
+ "set: $set/$iterations {$setMs}ms " .
+ "incr: $incr/$iterations {$incrMs}ms " .
+ "get: $get/$iterations ({$getMs}ms) " .
+ "delete: $delete/$iterations ({$delMs}ms)\n"
+ );
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ) {
+ $keysByValue = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keysByValue["test$i"] = 'S' . str_pad( $i, 2048 );
+ }
+ $keyList = array_keys( $keysByValue );
+
+ $time_start = microtime( true );
+ $mSetOk = $mcc->setMulti( $keysByValue ) ? 'S' : 'F';
+ $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $found = $mcc->getMulti( $keyList );
+ $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+ $mGetOk = 0;
+ foreach ( $found as $key => $value ) {
+ $mGetOk += ( $value === $keysByValue[$key] );
+ }
+
+ $time_start = microtime( true );
+ $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600 ) ? 'S' : 'F';
+ $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $mDelOk = $mcc->deleteMulti( $keyList ) ? 'S' : 'F';
+ $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " setMulti (IB): $mSetOk {$mSetMs}ms " .
+ "getMulti (IB): $mGetOk/$iterations {$mGetMs}ms " .
+ "changeTTLMulti (IB): $mChangeTTLOk {$mChangeTTTMs}ms " .
+ "deleteMulti (IB): $mDelOk {$mDelMs}ms\n"
+ );
+ }
+
+ /**
+ * @param BagOStuff $mcc
+ * @param int $iterations
+ */
+ private function benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ) {
+ $flags = $mcc::WRITE_BACKGROUND;
+ $keysByValue = [];
+ for ( $i = 1; $i <= $iterations; $i++ ) {
+ $keysByValue["test$i"] = 'A' . str_pad( $i, 2048 );
+ }
+ $keyList = array_keys( $keysByValue );
+
+ $time_start = microtime( true );
+ $mSetOk = $mcc->setMulti( $keysByValue, 0, $flags ) ? 'S' : 'F';
+ $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $found = $mcc->getMulti( $keyList );
+ $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+ $mGetOk = 0;
+ foreach ( $found as $key => $value ) {
+ $mGetOk += ( $value === $keysByValue[$key] );
+ }
+
+ $time_start = microtime( true );
+ $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600, $flags ) ? 'S' : 'F';
+ $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $time_start = microtime( true );
+ $mDelOk = $mcc->deleteMulti( $keyList, $flags ) ? 'S' : 'F';
+ $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
+
+ $this->output(
+ " setMulti (DB): $mSetOk {$mSetMs}ms " .
+ "getMulti (DB): $mGetOk/$iterations {$mGetMs}ms " .
+ "changeTTLMulti (DB): $mChangeTTLOk {$mChangeTTTMs}ms " .
+ "deleteMulti (DB): $mDelOk {$mDelMs}ms\n"
+ );
}
}
'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.jqueryMsg",
'packageFiles' => [
'mediawiki.jqueryMsg.js',
- [ 'name' => 'parserDefaults.json', 'callback' => function ( ResourceLoaderContext $context ) {
+ [ 'name' => 'parserDefaults.json', 'callback' => function (
+ ResourceLoaderContext $context, Config $config
+ ) {
$tagData = Sanitizer::getRecognizedTagData();
$allowedHtmlElements = array_merge(
array_keys( $tagData['htmlpairs'] ),
);
$magicWords = [
- 'SITENAME' => $context->getConfig()->get( 'Sitename' ),
+ 'SITENAME' => $config->get( 'Sitename' ),
];
Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] );
protected function setUp() {
parent::setUp();
$reflection = new ReflectionClass( $this );
- if ( strpos( $reflection->getFilename(), '/unit/' ) === false ) {
+ $dirSeparator = DIRECTORY_SEPARATOR;
+ if ( strpos( $reflection->getFilename(), "${dirSeparator}unit${dirSeparator}" ) === false ) {
$this->fail( 'This unit test needs to be in "tests/phpunit/unit" !' );
}
$this->unitGlobals = $GLOBALS;
*/
public function testWfGlobalCacheKey() {
$cache = ObjectCache::getLocalClusterInstance();
+ $this->hideDeprecated( 'wfGlobalCacheKey' );
$this->assertEquals(
$cache->makeGlobalKey( 'foo', 123, 'bar' ),
wfGlobalCacheKey( 'foo', 123, 'bar' )
namespace MediaWiki\Tests\Permissions;
use Action;
+use ContentHandler;
use FauxRequest;
-use MediaWiki\Session\SessionId;
-use MediaWiki\Session\TestUtils;
-use MediaWikiLangTestCase;
-use RequestContext;
-use stdClass;
-use Title;
-use User;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\SystemBlock;
+use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionLookup;
use Wikimedia\ScopedCallback;
+use MediaWiki\Session\SessionId;
+use MediaWiki\Session\TestUtils;
+use MediaWikiLangTestCase;
+use RequestContext;
+use stdClass;
+use Title;
+use User;
use Wikimedia\TestingAccessWrapper;
/**
);
}
+ public function testJsConfigRedirectEditPermissions() {
+ $revision = null;
+ $user = $this->getTestUser()->getUser();
+ $otherUser = $this->getTestUser( 'sysop' )->getUser();
+ $localJsTitle = Title::newFromText( 'User:' . $user->getName() . '/foo.js' );
+ $otherLocalJsTitle = Title::newFromText( 'User:' . $user->getName() . '/foo2.js' );
+ $nonlocalJsTitle = Title::newFromText( 'User:' . $otherUser->getName() . '/foo.js' );
+
+ $services = MediaWikiServices::getInstance();
+ $revisionLookup = $this->getMockBuilder( RevisionLookup::class )
+ ->setMethods( [ 'getRevisionByTitle' ] )
+ ->getMockForAbstractClass();
+ $revisionLookup->method( 'getRevisionByTitle' )
+ ->willReturnCallback( function ( LinkTarget $page ) use (
+ $services, &$revision, $localJsTitle
+ ) {
+ if ( $localJsTitle->equals( Title::newFromLinkTarget( $page ) ) ) {
+ return $revision;
+ } else {
+ return $services->getRevisionLookup()->getRevisionByTitle( $page );
+ }
+ } );
+ $permissionManager = new PermissionManager(
+ $services->getSpecialPageFactory(),
+ $revisionLookup,
+ [],
+ [],
+ false,
+ false,
+ [],
+ [],
+ [],
+ MediaWikiServices::getInstance()->getNamespaceInfo()
+ );
+ $this->setService( 'PermissionManager', $permissionManager );
+
+ $permissionManager->overrideUserRightsForTesting( $user, [ 'edit', 'editmyuserjs' ] );
+
+ $revision = $this->getJavascriptRevision( $localJsTitle, $user, '/* script */' );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $otherLocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $nonlocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [ [ 'mycustomjsredirectprotected', 'edit' ] ], $errors );
+
+ $permissionManager->overrideUserRightsForTesting( $user,
+ [ 'edit', 'editmyuserjs', 'editmyuserjsredirect' ] );
+
+ $revision = $this->getJavascriptRedirectRevision( $localJsTitle, $nonlocalJsTitle, $user );
+ $errors = $permissionManager->getPermissionErrors( 'edit', $user, $localJsTitle );
+ $this->assertSame( [], $errors );
+ }
+
/**
* @todo This test method should be split up into separate test methods and
* data providers
$this->assertFalse( $permissionManager->userHasRight( $this->user, 'move' ) );
}
+ /**
+ * Create a RevisionRecord with a single Javascript main slot.
+ * @param Title $title
+ * @param User $user
+ * @param string $text
+ * @return MutableRevisionRecord
+ */
+ private function getJavascriptRevision( Title $title, User $user, $text ) {
+ $content = ContentHandler::makeContent( $text, $title, CONTENT_MODEL_JAVASCRIPT );
+ $revision = new MutableRevisionRecord( $title );
+ $revision->setContent( 'main', $content );
+ return $revision;
+ }
+
+ /**
+ * Create a RevisionRecord with a single Javascript redirect main slot.
+ * @param Title $title
+ * @param Title $redirectTargetTitle
+ * @param User $user
+ * @return MutableRevisionRecord
+ */
+ private function getJavascriptRedirectRevision(
+ Title $title, Title $redirectTargetTitle, User $user
+ ) {
+ $content = ContentHandler::getForModelID( CONTENT_MODEL_JAVASCRIPT )
+ ->makeRedirectContent( $redirectTargetTitle );
+ $revision = new MutableRevisionRecord( $title );
+ $revision->setContent( 'main', $content );
+ return $revision;
+ }
+
}
$rev->getPage(),
$rev->getId()
);
- $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
+ $cache->delete( $key, WANObjectCache::HOLDOFF_TTL_NONE );
$this->assertFalse( $cache->get( $key ) );
++$now;
--- /dev/null
+<?php
+
+/**
+ * @group API
+ * @group medium
+ * @covers ApiCSPReport
+ */
+class ApiCSPReportTest extends MediaWikiIntegrationTestCase {
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( [
+ 'CSPFalsePositiveUrls' => [],
+ ] );
+ }
+
+ public function testInternalReportonly() {
+ $params = [
+ 'reportonly' => '1',
+ 'source' => 'internal',
+ ];
+ $cspReport = [
+ 'document-uri' => 'https://doc.test/path',
+ 'referrer' => 'https://referrer.test/path',
+ 'violated-directive' => 'connet-src',
+ 'disposition' => 'report',
+ 'blocked-uri' => 'https://blocked.test/path?query',
+ 'line-number' => 4,
+ 'column-number' => 2,
+ 'source-file' => 'https://source.test/path?query',
+ ];
+
+ $log = $this->doExecute( $params, $cspReport );
+
+ $this->assertEquals(
+ [
+ [
+ '[report-only] Received CSP report: ' .
+ '<https://blocked.test> blocked from being loaded on <https://doc.test/path>:4',
+ [
+ 'method' => 'ApiCSPReport::execute',
+ 'user_id' => 'logged-out',
+ 'user-agent' => 'Test/0.0',
+ 'source' => 'internal'
+ ]
+ ],
+ ],
+ $log,
+ 'logged messages'
+ );
+ }
+
+ public function testFalsePositiveOriginMatch() {
+ $params = [
+ 'reportonly' => '1',
+ 'source' => 'internal',
+ ];
+ $cspReport = [
+ 'document-uri' => 'https://doc.test/path',
+ 'referrer' => 'https://referrer.test/path',
+ 'violated-directive' => 'connet-src',
+ 'disposition' => 'report',
+ 'blocked-uri' => 'https://blocked.test/path/file?query',
+ 'line-number' => 4,
+ 'column-number' => 2,
+ 'source-file' => 'https://source.test/path/file?query',
+ ];
+
+ $this->setMwGlobals( [
+ 'wgCSPFalsePositiveUrls' => [
+ 'https://blocked.test/path/' => true,
+ ],
+ ] );
+ $log = $this->doExecute( $params, $cspReport );
+
+ $this->assertSame(
+ [],
+ $log,
+ 'logged messages'
+ );
+ }
+
+ private function doExecute( array $params, array $cspReport ) {
+ $log = [];
+ $logger = $this->createMock( Psr\Log\AbstractLogger::class );
+ $logger->method( 'warning' )->will( $this->returnCallback(
+ function ( $msg, $ctx ) use ( &$log ) {
+ unset( $ctx['csp-report'] );
+ $log[] = [ $msg, $ctx ];
+ }
+ ) );
+ $this->setLogger( 'csp-report-only', $logger );
+
+ $postBody = json_encode( [ 'csp-report' => $cspReport ] );
+ $req = $this->getMockBuilder( FauxRequest::class )
+ ->setMethods( [ 'getRawInput' ] )
+ ->setConstructorArgs( [ $params, /* $wasPosted */ true ] )
+ ->getMock();
+ $req->method( 'getRawInput' )->willReturn( $postBody );
+ $req->setHeaders( [
+ 'Content-Type' => 'application/csp-report',
+ 'User-Agent' => 'Test/0.0'
+ ] );
+
+ $api = $this->getMockBuilder( ApiCSPReport::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'getParameter', 'getRequest', 'getResult' ] )
+ ->getMock();
+ $api->method( 'getParameter' )->will( $this->returnCallback(
+ function ( $key ) use ( $req ) {
+ return $req->getRawVal( $key );
+ }
+ ) );
+ $api->method( 'getRequest' )->willReturn( $req );
+ $api->method( 'getResult' )->willReturn( new ApiResult( false ) );
+
+ $api->execute();
+ return $log;
+ }
+}
use MediaWiki\Block\BlockManager;
use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Block\CompositeBlock;
use MediaWiki\Block\SystemBlock;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
/**
* @group Blocking
* @covers ::shouldApplyCookieBlock
*/
public function testGetBlockFromCookieValue( $options, $expected ) {
- $blockManager = $this->getBlockManager( [
- 'wgCookieSetOnAutoblock' => true,
- 'wgCookieSetOnIpBlock' => true,
- ] );
+ $blockManager = TestingAccessWrapper::newFromObject(
+ $this->getBlockManager( [
+ 'wgCookieSetOnAutoblock' => true,
+ 'wgCookieSetOnIpBlock' => true,
+ ] )
+ );
$block = new DatabaseBlock( array_merge( [
- 'address' => $options[ 'target' ] ?: $this->user,
+ 'address' => $options['target'] ?: $this->user,
'by' => $this->sysopId,
- ], $options[ 'blockOptions' ] ) );
+ ], $options['blockOptions'] ) );
$block->insert();
- $class = new ReflectionClass( BlockManager::class );
- $method = $class->getMethod( 'getBlockFromCookieValue' );
- $method->setAccessible( true );
-
- $user = $options[ 'loggedIn' ] ? $this->user : new User();
+ $user = $options['loggedIn'] ? $this->user : new User();
$user->getRequest()->setCookie( 'BlockID', $block->getCookieValue() );
- $this->assertSame( $expected, (bool)$method->invoke(
- $blockManager,
+ $this->assertSame( $expected, (bool)$blockManager->getBlockFromCookieValue(
$user,
$user->getRequest()
) );
* @covers ::isLocallyBlockedProxy
*/
public function testIsLocallyBlockedProxy( $proxyList, $expected ) {
- $class = new ReflectionClass( BlockManager::class );
- $method = $class->getMethod( 'isLocallyBlockedProxy' );
- $method->setAccessible( true );
-
- $blockManager = $this->getBlockManager( [
- 'wgProxyList' => $proxyList
- ] );
+ $blockManager = TestingAccessWrapper::newFromObject(
+ $this->getBlockManager( [
+ 'wgProxyList' => $proxyList
+ ] )
+ );
$ip = '1.2.3.4';
- $this->assertSame( $expected, $method->invoke( $blockManager, $ip ) );
+ $this->assertSame( $expected, $blockManager->isLocallyBlockedProxy( $ip ) );
}
public static function provideIsLocallyBlockedProxy() {
'addresses in keys: ' . $proxy . ', please move them to values)'
);
- $class = new ReflectionClass( BlockManager::class );
- $method = $class->getMethod( 'isLocallyBlockedProxy' );
- $method->setAccessible( true );
-
- $blockManager = $this->getBlockManager( [
- 'wgProxyList' => [ $proxy => 'test' ]
- ] );
+ $blockManager = TestingAccessWrapper::newFromObject(
+ $this->getBlockManager( [
+ 'wgProxyList' => [ $proxy => 'test' ]
+ ] )
+ );
$ip = '1.2.3.4';
- $this->assertSame( true, $method->invoke( $blockManager, $ip ) );
+ $this->assertTrue( $blockManager->isLocallyBlockedProxy( $ip ) );
}
/**
->setConstructorArgs( $this->getBlockManagerConstructorArgs( $blockManagerConfig ) )
->setMethods( [ 'checkHost' ] )
->getMock();
-
- $blockManager->expects( $this->any() )
- ->method( 'checkHost' )
+ $blockManager->method( 'checkHost' )
->will( $this->returnValueMap( [ [
$options['dnsblQuery'],
$options['dnsblResponse'],
public function testGetUniqueBlocks() {
$blockId = 100;
- $class = new ReflectionClass( BlockManager::class );
- $method = $class->getMethod( 'getUniqueBlocks' );
- $method->setAccessible( true );
-
- $blockManager = $this->getBlockManager( [] );
+ $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
$block = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getId' ] )
->getMock();
- $block->expects( $this->any() )
- ->method( 'getId' )
+ $block->method( 'getId' )
->willReturn( $blockId );
$autoblock = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getParentBlockId', 'getType' ] )
->getMock();
- $autoblock->expects( $this->any() )
- ->method( 'getParentBlockId' )
+ $autoblock->method( 'getParentBlockId' )
->willReturn( $blockId );
- $autoblock->expects( $this->any() )
- ->method( 'getType' )
+ $autoblock->method( 'getType' )
->willReturn( DatabaseBlock::TYPE_AUTO );
$blocks = [ $block, $block, $autoblock, new SystemBlock() ];
- $this->assertSame( 2, count( $method->invoke( $blockManager, $blocks ) ) );
+ $this->assertSame( 2, count( $blockManager->getUniqueBlocks( $blocks ) ) );
}
/**
- * @covers ::trackBlockWithCookie
* @dataProvider provideTrackBlockWithCookie
- * @param bool $expectCookieSet
- * @param bool $hasCookie
- * @param bool $isBlocked
+ * @covers ::trackBlockWithCookie
*/
- public function testTrackBlockWithCookie( $expectCookieSet, $hasCookie, $isBlocked ) {
- $blockID = 123;
+ public function testTrackBlockWithCookie( $options, $expectedVal ) {
$this->setMwGlobals( 'wgCookiePrefix', '' );
$request = new FauxRequest();
- if ( $hasCookie ) {
+ if ( $options['cookieSet'] ) {
$request->setCookie( 'BlockID', 'the value does not matter' );
}
- if ( $isBlocked ) {
- $block = $this->getMockBuilder( DatabaseBlock::class )
- ->setMethods( [ 'getType', 'getId' ] )
- ->getMock();
- $block->method( 'getType' )
- ->willReturn( DatabaseBlock::TYPE_IP );
- $block->method( 'getId' )
- ->willReturn( $blockID );
- } else {
- $block = null;
- }
-
$user = $this->getMockBuilder( User::class )
->setMethods( [ 'getBlock', 'getRequest' ] )
->getMock();
$user->method( 'getBlock' )
- ->willReturn( $block );
+ ->willReturn( $options['block'] );
$user->method( 'getRequest' )
->willReturn( $request );
- /** @var User $user */
// Although the block cookie is set via DeferredUpdates, in command line mode updates are
// processed immediately
- $blockManager = $this->getBlockManager( [] );
+ $blockManager = $this->getBlockManager( [
+ 'wgSecretKey' => '',
+ 'wgCookieSetOnIpBlock' => true,
+ ] );
$blockManager->trackBlockWithCookie( $user );
/** @var FauxResponse $response */
$response = $request->response();
- $this->assertCount( $expectCookieSet ? 1 : 0, $response->getCookies() );
- $this->assertEquals( $expectCookieSet ? $blockID : null, $response->getCookie( 'BlockID' ) );
+ $this->assertCount( $expectedVal ? 1 : 0, $response->getCookies() );
+ $this->assertEquals( $expectedVal ?: null, $response->getCookie( 'BlockID' ) );
}
public function provideTrackBlockWithCookie() {
+ $blockId = 123;
+ return [
+ 'Block cookie is already set; there is a trackable block' => [
+ [
+ 'cookieSet' => true,
+ 'block' => $this->getTrackableBlock( $blockId ),
+ ],
+ null,
+ ],
+ 'Block cookie is already set; there is no block' => [
+ [
+ 'cookieSet' => true,
+ 'block' => null,
+ ],
+ null,
+ ],
+ 'Block cookie is not yet set; there is no block' => [
+ [
+ 'cookieSet' => false,
+ 'block' => null,
+ ],
+ null,
+ ],
+ 'Block cookie is not yet set; there is a trackable block' => [
+ [
+ 'cookieSet' => false,
+ 'block' => $this->getTrackableBlock( $blockId ),
+ ],
+ $blockId,
+ ],
+ 'Block cookie is not yet set; there is a composite block with a trackable block' => [
+ [
+ 'cookieSet' => false,
+ 'block' => new CompositeBlock( [
+ 'originalBlocks' => [
+ new SystemBlock(),
+ $this->getTrackableBlock( $blockId ),
+ ]
+ ] ),
+ ],
+ $blockId,
+ ],
+ 'Block cookie is not yet set; there is a composite block but no trackable block' => [
+ [
+ 'cookieSet' => false,
+ 'block' => new CompositeBlock( [
+ 'originalBlocks' => [
+ new SystemBlock(),
+ new SystemBlock(),
+ ]
+ ] ),
+ ],
+ null,
+ ],
+ ];
+ }
+
+ private function getTrackableBlock( $blockId ) {
+ $block = $this->getMockBuilder( DatabaseBlock::class )
+ ->setMethods( [ 'getType', 'getId' ] )
+ ->getMock();
+ $block->method( 'getType' )
+ ->willReturn( DatabaseBlock::TYPE_IP );
+ $block->method( 'getId' )
+ ->willReturn( $blockId );
+ return $block;
+ }
+
+ /**
+ * @dataProvider provideSetBlockCookie
+ * @covers ::setBlockCookie
+ */
+ public function testSetBlockCookie( $expiryDelta, $expectedExpiryDelta ) {
+ $this->setMwGlobals( [
+ 'wgCookiePrefix' => '',
+ ] );
+
+ $request = new FauxRequest();
+ $response = $request->response();
+
+ $blockManager = $this->getBlockManager( [
+ 'wgSecretKey' => '',
+ 'wgCookieSetOnIpBlock' => true,
+ ] );
+
+ $now = wfTimestamp();
+
+ $block = new DatabaseBlock( [
+ 'expiry' => $expiryDelta === '' ? '' : $now + $expiryDelta
+ ] );
+ $blockManager->setBlockCookie( $block, $response );
+ $cookies = $response->getCookies();
+
+ $this->assertEquals(
+ $now + $expectedExpiryDelta,
+ $cookies['BlockID']['expire'],
+ '',
+ 60 // Allow actual to be up to 60 seconds later than expected
+ );
+ }
+
+ public static function provideSetBlockCookie() {
+ // Maximum length of a block cookie, defined in BlockManager::setBlockCookie
+ $maxExpiryDelta = ( 24 * 60 * 60 );
+
+ $longExpiryDelta = ( 48 * 60 * 60 );
+ $shortExpiryDelta = ( 12 * 60 * 60 );
+
+ return [
+ 'Block has indefinite expiry' => [
+ '',
+ $maxExpiryDelta,
+ ],
+ 'Block expiry is later than maximum cookie block expiry' => [
+ $longExpiryDelta,
+ $maxExpiryDelta,
+ ],
+ 'Block expiry is sooner than maximum cookie block expiry' => [
+ $shortExpiryDelta,
+ $shortExpiryDelta,
+ ],
+ ];
+ }
+
+ /**
+ * @covers ::shouldTrackBlockWithCookie
+ */
+ public function testShouldTrackBlockWithCookieSystemBlock() {
+ $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
+ $this->assertFalse( $blockManager->shouldTrackBlockWithCookie(
+ new SystemBlock(),
+ true
+ ) );
+ }
+
+ /**
+ * @dataProvider provideShouldTrackBlockWithCookie
+ * @covers ::shouldTrackBlockWithCookie
+ */
+ public function testShouldTrackBlockWithCookie( $options, $expected ) {
+ $block = $this->getMockBuilder( DatabaseBlock::class )
+ ->setMethods( [ 'getType', 'isAutoblocking' ] )
+ ->getMock();
+ $block->method( 'getType' )
+ ->willReturn( $options['type'] );
+ if ( isset( $options['autoblocking'] ) ) {
+ $block->method( 'isAutoblocking' )
+ ->willReturn( $options['autoblocking'] );
+ }
+
+ $blockManager = TestingAccessWrapper::newFromObject(
+ $this->getBlockManager( $options['blockManagerConfig'] )
+ );
+
+ $this->assertSame(
+ $expected,
+ $blockManager->shouldTrackBlockWithCookie( $block, $options['isAnon'] )
+ );
+ }
+
+ public static function provideShouldTrackBlockWithCookie() {
return [
- // $expectCookieSet, $hasCookie, $isBlocked
- [ false, false, false ],
- [ false, true, false ],
- [ true, false, true ],
- [ false, true, true ],
+ 'IP block, anonymous user, IP block cookies enabled' => [
+ [
+ 'type' => DatabaseBlock::TYPE_IP,
+ 'isAnon' => true,
+ 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
+ ],
+ true
+ ],
+ 'IP range block, anonymous user, IP block cookies enabled' => [
+ [
+ 'type' => DatabaseBlock::TYPE_RANGE,
+ 'isAnon' => true,
+ 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
+ ],
+ true
+ ],
+ 'IP block, anonymous user, IP block cookies disabled' => [
+ [
+ 'type' => DatabaseBlock::TYPE_IP,
+ 'isAnon' => true,
+ 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => false ],
+ ],
+ false
+ ],
+ 'IP block, logged in user, IP block cookies enabled' => [
+ [
+ 'type' => DatabaseBlock::TYPE_IP,
+ 'isAnon' => false,
+ 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
+ ],
+ false
+ ],
+ 'User block, anonymous, autoblock cookies enabled, block is autoblocking' => [
+ [
+ 'type' => DatabaseBlock::TYPE_USER,
+ 'isAnon' => true,
+ 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
+ 'autoblocking' => true,
+ ],
+ false
+ ],
+ 'User block, logged in, autoblock cookies enabled, block is autoblocking' => [
+ [
+ 'type' => DatabaseBlock::TYPE_USER,
+ 'isAnon' => false,
+ 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
+ 'autoblocking' => true,
+ ],
+ true
+ ],
+ 'User block, logged in, autoblock cookies disabled, block is autoblocking' => [
+ [
+ 'type' => DatabaseBlock::TYPE_USER,
+ 'isAnon' => false,
+ 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => false ],
+ 'autoblocking' => true,
+ ],
+ false
+ ],
+ 'User block, logged in, autoblock cookies enabled, block is not autoblocking' => [
+ [
+ 'type' => DatabaseBlock::TYPE_USER,
+ 'isAnon' => false,
+ 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
+ 'autoblocking' => false,
+ ],
+ false
+ ],
+ 'Block type is autoblock' => [
+ [
+ 'type' => DatabaseBlock::TYPE_AUTO,
+ 'isAnon' => true,
+ 'blockManagerConfig' => [],
+ ],
+ false
+ ]
];
}
+
+ /**
+ * @covers ::clearBlockCookie
+ */
+ public function testClearBlockCookie() {
+ $this->setMwGlobals( [
+ 'wgCookiePrefix' => '',
+ ] );
+
+ $request = new FauxRequest();
+ $response = $request->response();
+ $response->setCookie( 'BlockID', '100' );
+ $this->assertSame( '100', $response->getCookie( 'BlockID' ) );
+
+ BlockManager::clearBlockCookie( $response );
+ $this->assertSame( '', $response->getCookie( 'BlockID' ) );
+ }
+
+ /**
+ * @dataProvider provideGetIdFromCookieValue
+ * @covers ::getIdFromCookieValue
+ */
+ public function testGetIdFromCookieValue( $options, $expected ) {
+ $blockManager = $this->getBlockManager( [
+ 'wgSecretKey' => $options['secretKey']
+ ] );
+ $this->assertEquals(
+ $expected,
+ $blockManager->getIdFromCookieValue( $options['cookieValue'] )
+ );
+ }
+
+ public static function provideGetIdFromCookieValue() {
+ $blockId = 100;
+ $secretKey = '123';
+ $hmac = MWCryptHash::hmac( $blockId, $secretKey, false );
+ return [
+ 'No secret key is set' => [
+ [
+ 'secretKey' => '',
+ 'cookieValue' => $blockId,
+ 'calculatedHmac' => MWCryptHash::hmac( $blockId, '', false ),
+ ],
+ $blockId,
+ ],
+ 'Secret key is set and stored hmac is correct' => [
+ [
+ 'secretKey' => $secretKey,
+ 'cookieValue' => $blockId . '!' . $hmac,
+ 'calculatedHmac' => $hmac,
+ ],
+ $blockId,
+ ],
+ 'Secret key is set and stored hmac is incorrect' => [
+ [
+ 'secretKey' => $secretKey,
+ 'cookieValue' => $blockId . '!xyz',
+ 'calculatedHmac' => $hmac,
+ ],
+ null,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetCookieValue
+ * @covers ::getCookieValue
+ */
+ public function testGetCookieValue( $options, $expected ) {
+ $blockManager = $this->getBlockManager( [
+ 'wgSecretKey' => $options['secretKey']
+ ] );
+
+ $block = $this->getMockBuilder( DatabaseBlock::class )
+ ->setMethods( [ 'getId' ] )
+ ->getMock();
+ $block->method( 'getId' )
+ ->willReturn( $options['blockId'] );
+
+ $this->assertEquals(
+ $expected,
+ $blockManager->getCookieValue( $block )
+ );
+ }
+
+ public static function provideGetCookieValue() {
+ $blockId = 100;
+ return [
+ 'Secret key not set' => [
+ [
+ 'secretKey' => '',
+ 'blockId' => $blockId,
+ 'hmac' => MWCryptHash::hmac( $blockId, '', false ),
+ ],
+ $blockId,
+ ],
+ 'Secret key set' => [
+ [
+ 'secretKey' => '123',
+ 'blockId' => $blockId,
+ 'hmac' => MWCryptHash::hmac( $blockId, '123', false ),
+ ],
+ $blockId . '!' . MWCryptHash::hmac( $blockId, '123', false ) ],
+ ];
+ }
+
}
];
}
- public function testNoDBAccess() {
+ public function testNoDBAccessContentLanguage() {
global $wgContLanguageCode;
$dbr = wfGetDB( DB_REPLICA );
$dbr->restoreFlags();
- $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries" );
+ $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries (content language)" );
+ }
+
+ public function testNoDBAccessNonContentLanguage() {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ MessageCache::singleton()->getMsgFromNamespace( 'allpages/nl', 'nl' );
+
+ $this->assertEquals( 0, $dbr->trxLevel() );
+ $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX
+
+ MessageCache::singleton()->getMsgFromNamespace( 'go/nl', 'nl' );
+
+ $dbr->restoreFlags();
+
+ $this->assertEquals( 0, $dbr->trxLevel(), "No DB read queries (non-content language)" );
}
/**
/**
* @author Matthias Mullie <mmullie@wikimedia.org>
* @group BagOStuff
+ * @covers BagOStuff
*/
class BagOStuffTest extends MediaWikiTestCase {
/** @var BagOStuff */
}
/**
- * @covers BagOStuff::makeGlobalKey
- * @covers BagOStuff::makeKeyInternal
+ * @covers MediumSpecificBagOStuff::makeGlobalKey
+ * @covers MediumSpecificBagOStuff::makeKeyInternal
*/
public function testMakeKey() {
$cache = ObjectCache::newFromId( 'hash' );
}
/**
- * @covers BagOStuff::merge
- * @covers BagOStuff::mergeViaCas
+ * @covers MediumSpecificBagOStuff::merge
+ * @covers MediumSpecificBagOStuff::mergeViaCas
*/
public function testMerge() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::changeTTL
+ * @covers MediumSpecificBagOStuff::changeTTL
*/
public function testChangeTTL() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::changeTTLMulti
+ * @covers MediumSpecificBagOStuff::changeTTLMulti
*/
public function testChangeTTLMulti() {
$key1 = $this->cache->makeKey( 'test-key1' );
}
/**
- * @covers BagOStuff::add
+ * @covers MediumSpecificBagOStuff::add
*/
public function testAdd() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::get
+ * @covers MediumSpecificBagOStuff::get
*/
public function testGet() {
$value = [ 'this' => 'is', 'a' => 'test' ];
}
/**
- * @covers BagOStuff::get
- * @covers BagOStuff::set
- * @covers BagOStuff::getWithSetCallback
+ * @covers MediumSpecificBagOStuff::get
+ * @covers MediumSpecificBagOStuff::set
+ * @covers MediumSpecificBagOStuff::getWithSetCallback
*/
public function testGetWithSetCallback() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::incr
+ * @covers MediumSpecificBagOStuff::incr
*/
public function testIncr() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::incrWithInit
+ * @covers MediumSpecificBagOStuff::incrWithInit
*/
public function testIncrWithInit() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::getMulti
+ * @covers MediumSpecificBagOStuff::getMulti
*/
public function testGetMulti() {
$value1 = [ 'this' => 'is', 'a' => 'test' ];
}
/**
- * @covers BagOStuff::setMulti
- * @covers BagOStuff::deleteMulti
+ * @covers MediumSpecificBagOStuff::setMulti
+ * @covers MediumSpecificBagOStuff::deleteMulti
*/
public function testSetDeleteMulti() {
$map = [
}
/**
- * @covers BagOStuff::get
- * @covers BagOStuff::getMulti
- * @covers BagOStuff::merge
- * @covers BagOStuff::delete
+ * @covers MediumSpecificBagOStuff::get
+ * @covers MediumSpecificBagOStuff::getMulti
+ * @covers MediumSpecificBagOStuff::merge
+ * @covers MediumSpecificBagOStuff::delete
*/
public function testSetSegmentable() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::getScopedLock
+ * @covers MediumSpecificBagOStuff::getScopedLock
*/
public function testGetScopedLock() {
$key = $this->cache->makeKey( self::TEST_KEY );
}
/**
- * @covers BagOStuff::__construct
- * @covers BagOStuff::trackDuplicateKeys
+ * @covers MediumSpecificBagOStuff::__construct
+ * @covers MediumSpecificBagOStuff::trackDuplicateKeys
*/
public function testReportDupes() {
$logger = $this->createMock( Psr\Log\NullLogger::class );
}
/**
- * @covers BagOStuff::lock()
- * @covers BagOStuff::unlock()
+ * @covers MediumSpecificBagOStuff::lock()
+ * @covers MediumSpecificBagOStuff::unlock()
*/
public function testLocking() {
$key = 'test';
$localBag = $this->getMockBuilder( HashBagOStuff::class )
->setMethods( [ 'getMulti' ] )->getMock();
$localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
- WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1',
- WANObjectCache::VALUE_KEY_PREFIX . 'k2' => 'val-id2'
+ 'WANCache:v:' . 'k1' => 'val-id1',
+ 'WANCache:v:' . 'k2' => 'val-id2'
] );
$wanCache = new WANObjectCache( [ 'cache' => $localBag ] );
$this->assertEquals( 1, $calls, 'Value was populated' );
// Acquire the mutex to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$mockWallClock += 2; // low logical TTL expired
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
$this->assertEquals( $value, $ret );
$mockWallClock += 301; // physical TTL expired
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
$this->assertEquals( $value, $ret );
$mockWallClock += 0.2; // interim keys not brand new
// Acquire a lock to verify that getWithSetCallback uses busyValue properly
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
$this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
- $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
+ $this->internalCache->delete( 'WANCache:m:' . $key );
$mockWallClock += 0.001; // cached values will be newer than tombstone
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
$this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
// Fake initial check key to be set in the past. Otherwise we'd have to sleep for
// several seconds during the test to assert the behaviour.
foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
- $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE );
+ $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_TTL_NONE );
}
$mockWallClock += 0.100;
$this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
$this->cache->set( $key, $value );
- $this->cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
+ $this->cache->delete( $key, WANObjectCache::HOLDOFF_TTL_NONE );
$curTTL = null;
$v = $this->cache->get( $key, $curTTL );
$v = $cache->getWithSetCallback( $key, 60, $func );
$this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
// Lock up the mutex so interim cache is used
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$v = $cache->getWithSetCallback( $key, 60, $func );
$this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' );
- $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
+ $this->internalCache->delete( 'WANCache:m:' . $key );
$cache->useInterimHoldOffCaching( false );
$v = $cache->getWithSetCallback( $key, 60, $func );
$this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' );
// Lock up the mutex so interim cache is used
- $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
$v = $cache->getWithSetCallback( $key, 60, $func );
$this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' );
}
// Two check keys are newer (given hold-off) than $key, another is older
$this->internalCache->set(
- WANObjectCache::TIME_KEY_PREFIX . $tKey2,
- WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 3 )
+ 'WANCache:t:' . $tKey2,
+ 'PURGED:' . ( $priorTime - 3 )
);
$this->internalCache->set(
- WANObjectCache::TIME_KEY_PREFIX . $tKey2,
- WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 5 )
+ 'WANCache:t:' . $tKey2,
+ 'PURGED:' . ( $priorTime - 5 )
);
$this->internalCache->set(
- WANObjectCache::TIME_KEY_PREFIX . $tKey1,
- WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 30 )
+ 'WANCache:t:' . $tKey1,
+ 'PURGED:' . ( $priorTime - 30 )
);
$this->cache->set( $key, $value, 30 );
$badTime = microtime( true ) - 300;
$this->internalCache->set(
- WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
+ 'WANCache:v:' . $vKey1,
[
- WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
- WANObjectCache::FLD_VALUE => $value,
- WANObjectCache::FLD_TTL => 3600,
- WANObjectCache::FLD_TIME => $goodTime
+ 0 => 1,
+ 1 => $value,
+ 2 => 3600,
+ 3 => $goodTime
]
);
$this->internalCache->set(
- WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
+ 'WANCache:v:' . $vKey2,
[
- WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
- WANObjectCache::FLD_VALUE => $value,
- WANObjectCache::FLD_TTL => 3600,
- WANObjectCache::FLD_TIME => $badTime
+ 0 => 1,
+ 1 => $value,
+ 2 => 3600,
+ 3 => $badTime
]
);
$this->internalCache->set(
- WANObjectCache::TIME_KEY_PREFIX . $tKey1,
- WANObjectCache::PURGE_VAL_PREFIX . $goodTime
+ 'WANCache:t:' . $tKey1,
+ 'PURGED:' . $goodTime
);
$this->internalCache->set(
- WANObjectCache::TIME_KEY_PREFIX . $tKey2,
- WANObjectCache::PURGE_VAL_PREFIX . $badTime
+ 'WANCache:t:' . $tKey2,
+ 'PURGED:' . $badTime
);
$this->assertEquals( $value, $this->cache->get( $vKey1 ) );
->setMethods( [ 'get', 'changeTTL' ] )->getMock();
$backend->expects( $this->once() )->method( 'get' )
->willReturn( [
- WANObjectCache::FLD_FORMAT_VERSION => WANObjectCache::VERSION,
- WANObjectCache::FLD_VALUE => 'value',
- WANObjectCache::FLD_TTL => 3600,
- WANObjectCache::FLD_TIME => 300,
+ 0 => 1,
+ 1 => 'value',
+ 2 => 3600,
+ 3 => 300,
] );
$backend->expects( $this->once() )->method( 'changeTTL' )
->willReturn( false );
] );
$localBag->expects( $this->once() )->method( 'set' )
- ->with( "/*/mw-wan/" . $wanCache::VALUE_KEY_PREFIX . "test" );
+ ->with( "/*/mw-wan/" . 'WANCache:v:' . "test" );
$wanCache->delete( 'test' );
}
] );
$localBag->expects( $this->once() )->method( 'set' )
- ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
+ ->with( "/*/mw-wan/" . 'WANCache:t:' . "test" );
$wanCache->touchCheckKey( 'test' );
}
] );
$localBag->expects( $this->once() )->method( 'delete' )
- ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
+ ->with( "/*/mw-wan/" . 'WANCache:t:' . "test" );
$wanCache->resetCheckKey( 'test' );
}
+++ /dev/null
-<?php
-
-class ResourceLoaderFilePathTest extends PHPUnit\Framework\TestCase {
- /**
- * @covers ResourceLoaderFilePath::__construct
- */
- public function testConstructor() {
- $resourceLoaderFilePath = new ResourceLoaderFilePath(
- 'dummy/path', 'localBasePath', 'remoteBasePath'
- );
-
- $this->assertInstanceOf( ResourceLoaderFilePath::class, $resourceLoaderFilePath );
- }
-
- /**
- * @covers ResourceLoaderFilePath::getLocalPath
- */
- public function testGetLocalPath() {
- $resourceLoaderFilePath = new ResourceLoaderFilePath(
- 'dummy/path', 'localBasePath', 'remoteBasePath'
- );
-
- $this->assertSame(
- 'localBasePath/dummy/path', $resourceLoaderFilePath->getLocalPath()
- );
- }
-
- /**
- * @covers ResourceLoaderFilePath::getRemotePath
- */
- public function testGetRemotePath() {
- $resourceLoaderFilePath = new ResourceLoaderFilePath(
- 'dummy/path', 'localBasePath', 'remoteBasePath'
- );
-
- $this->assertSame(
- 'remoteBasePath/dummy/path', $resourceLoaderFilePath->getRemotePath()
- );
- }
-
- /**
- * @covers ResourceLoaderFilePath::getPath
- */
- public function testGetPath() {
- $resourceLoaderFilePath = new ResourceLoaderFilePath(
- 'dummy/path', 'localBasePath', 'remoteBasePath'
- );
-
- $this->assertSame(
- 'dummy/path', $resourceLoaderFilePath->getPath()
- );
- }
-}
namespace MediaWiki\Session;
+use CachedBagOStuff;
+use HashBagOStuff;
+use RequestContext;
+
/**
* BagOStuff with utility functions for MediaWiki\\Session\\* testing
*/
-class TestBagOStuff extends \CachedBagOStuff {
+class TestBagOStuff extends CachedBagOStuff {
public function __construct() {
- parent::__construct( new \HashBagOStuff );
+ parent::__construct( new HashBagOStuff );
}
/**
* @param array|mixed $blob Session metadata and data
*/
public function setRawSession( $id, $blob ) {
- $expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' );
+ $expiry = RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' );
$this->set( $this->makeKey( 'MWSession', $id ), $blob, $expiry );
}
/**
* @covers SpecialMute::execute
- * @expectedExceptionMessage Muting users from sending you emails is not enabled
+ * @expectedExceptionMessage Mute features are unavailable
* @expectedException ErrorPageError
*/
public function testEmailBlacklistNotEnabled() {
+ $this->setTemporaryHook(
+ 'SpecialMuteModifyFormFields',
+ null
+ );
+
$this->setMwGlobals( [
'wgEnableUserEmailBlacklist' => false
] );
$loggedInUser->confirmEmail();
$loggedInUser->saveSettings();
- $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => 1 ], true );
+ $fauxRequest = new FauxRequest( [ 'wpemail-blacklist' => true ], true );
list( $html, ) = $this->executeSpecialPage(
$targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser
);
$loggedInUser->confirmEmail();
$loggedInUser->saveSettings();
- $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => false ], true );
+ $fauxRequest = new FauxRequest( [ 'wpemail-blacklist' => false ], true );
list( $html, ) = $this->executeSpecialPage(
$targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser
);
$this->assertEquals(
'wiki',
$this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
- 'get(): simple setting on an non-existing wiki'
+ 'get(): simple setting on a non-existing wiki'
);
// Fallback
$this->assertEquals(
'wiki',
$this->mConf->get( 'Fallback', 'eswiki', 'wiki' ),
- 'get(): fallback setting on an non-existing wiki'
+ 'get(): fallback setting on a non-existing wiki'
);
$this->assertEquals(
'tag',
$this->mConf->get( 'Fallback', 'eswiki', 'wiki', [], [ 'tag' ] ),
- 'get(): fallback setting on an non-existing wiki (with wiki tag)'
+ 'get(): fallback setting on a non-existing wiki (with wiki tag)'
);
// Merging
$this->assertEquals(
$common,
$this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
- 'get(): merging setting on an non-existing wiki'
+ 'get(): merging setting on a non-existing wiki'
);
$this->assertEquals(
$commonTag,
$this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
- 'get(): merging setting on an non-existing wiki (with tag)'
+ 'get(): merging setting on a non-existing wiki (with tag)'
);
}
$this->assertEquals(
'es wiki eswiki',
$this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
- 'get(): parameter replacement on an non-existing wiki'
+ 'get(): parameter replacement on a non-existing wiki'
);
}
--- /dev/null
+<?php
+
+/**
+ * @covers ResourceLoaderFilePath
+ */
+class ResourceLoaderFilePathTest extends MediaWikiUnitTestCase {
+
+ public function testConstructor() {
+ $path = new ResourceLoaderFilePath( 'dummy/path', '/local', '/remote' );
+
+ $this->assertInstanceOf( ResourceLoaderFilePath::class, $path );
+ }
+
+ public function testGetters() {
+ $path = new ResourceLoaderFilePath( 'dummy/path', '/local', '/remote' );
+
+ $this->assertSame( '/local/dummy/path', $path->getLocalPath() );
+ $this->assertSame( '/remote/dummy/path', $path->getRemotePath() );
+ $this->assertSame( '/local', $path->getLocalBasePath() );
+ $this->assertSame( '/remote', $path->getRemoteBasePath() );
+ $this->assertSame( 'dummy/path', $path->getPath() );
+ }
+}
+## 0.4.0 / 2019-07-18
+
+* Util: Added a `waitForModuleState()` method.
+* Api: Added optional `username`, `password` and `baseUrl` parameters to `edit()` method.
+* RunJobs: Unpublished `getJobCount()`, `log()`, `runThroughMainPageRequests()` methods.
+
## 0.3.0 / 2019-01-25
* RunJobs: Added initial version.
Actions are performed logged-in using `browser.options.username` and `browser.options.password`,
which typically come from `MEDIAWIKI_USER` and `MEDIAWIKI_PASSWORD` environment variables.
-* `edit(title, content)`
+* `edit(title, content [, string username [, string password [, string baseUrl ] ] ])`
* `delete(title, reason)`
* `createAccount(username, password)`
* `blockUser(username, expiry)`
Use the static `RunJobs.run()` method to ensure that any queued jobs are executed before
making assertions that depend on its outcome.
+### Util
+
+`Util` is a collection of popular utility methods.
+
+* `getTestString([ string prefix ])`
+* `waitForModuleState(string moduleName [, string moduleStatus [, number timeout ] ])`
+
## Versioning
This package follows [Semantic Versioning guidelines](https://semver.org/) for its releases. In
{
"name": "wdio-mediawiki",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "WebdriverIO plugin for testing a MediaWiki site.",
"homepage": "https://gerrit.wikimedia.org/g/mediawiki/core/+/master/tests/selenium/wdio-mediawiki/",
"license": "MIT",