From: jenkins-bot Date: Fri, 15 Mar 2019 21:46:07 +0000 (+0000) Subject: Merge "Fix WatchedItemStore last-seen stashing logic" X-Git-Tag: 1.34.0-rc.0~2508 X-Git-Url: https://git.heureux-cyclage.org/?a=commitdiff_plain;h=4d1478c76fc650787ee9d7fb12c7ee62a707e907;hp=588a4646825236502270b08ff05c53434abbaebc;p=lhc%2Fweb%2Fwiklou.git Merge "Fix WatchedItemStore last-seen stashing logic" --- diff --git a/RELEASE-NOTES-1.33 b/RELEASE-NOTES-1.33 index f8a4db1ada..27b1c71a05 100644 --- a/RELEASE-NOTES-1.33 +++ b/RELEASE-NOTES-1.33 @@ -287,6 +287,8 @@ because of Phabricator reports. * BagOStuff::modifySimpleRelayEvent() method has been removed. * ParserOutput::getLegacyOptions, deprecated in 1.30, has been removed. Use ParserOutput::allCacheVaryingOptions instead. +* CdnCacheUpdate::newSimplePurge, deprecated in 1.27, has been removed. + Use CdnCacheUpdate::newFromTitles() instead. === Deprecations in 1.33 === * The configuration option $wgUseESI has been deprecated, and is expected @@ -352,6 +354,7 @@ because of Phabricator reports. * The implementation of buildStringCast() in Wikimedia\Rdbms\Database has changed to explicitly cast. Subclasses relying on the base-class implementation should check whether they need to override it now. +* BagOStuff::add is now abstract and must explicitly be defined in subclasses. == Compatibility == MediaWiki 1.33 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is diff --git a/autoload.php b/autoload.php index 9dad6f2b1f..bb4de22cce 100644 --- a/autoload.php +++ b/autoload.php @@ -1215,6 +1215,7 @@ $wgAutoloadLocalClasses = [ 'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php', 'RefreshLinks' => __DIR__ . '/maintenance/refreshLinks.php', 'RefreshLinksJob' => __DIR__ . '/includes/jobqueue/jobs/RefreshLinksJob.php', + 'RefreshSecondaryDataUpdate' => __DIR__ . '/includes/deferred/RefreshSecondaryDataUpdate.php', 'RegexlikeReplacer' => __DIR__ . '/includes/libs/replacers/RegexlikeReplacer.php', 'RemexStripTagHandler' => __DIR__ . '/includes/parser/RemexStripTagHandler.php', 'RemoveInvalidEmails' => __DIR__ . '/maintenance/removeInvalidEmails.php', diff --git a/includes/ForeignResourceManager.php b/includes/ForeignResourceManager.php index 18014fa528..e0d088a916 100644 --- a/includes/ForeignResourceManager.php +++ b/includes/ForeignResourceManager.php @@ -234,6 +234,7 @@ class ForeignResourceManager { $from, RecursiveDirectoryIterator::SKIP_DOTS ) ); + /** @var SplFileInfo $file */ foreach ( $rii as $file ) { $remote = $file->getPathname(); $local = strtr( $remote, [ $from => $to ] ); diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 9ce12b4b13..8dedc70211 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -24,6 +24,7 @@ namespace MediaWiki\Storage; use ApiStashEdit; use CategoryMembershipChangeJob; +use RefreshSecondaryDataUpdate; use Content; use ContentHandler; use DataUpdate; @@ -1590,14 +1591,31 @@ class DerivedPageDataUpdater implements IDBAccessObject { $update->setRevision( $legacyRevision ); $update->setTriggeringUser( $triggeringUser ); } - if ( $options['defer'] === false ) { - if ( $options['transactionTicket'] !== null ) { + } + + if ( $options['defer'] === false ) { + foreach ( $updates as $update ) { + if ( $update instanceof DataUpdate && $options['transactionTicket'] !== null ) { $update->setTransactionTicket( $options['transactionTicket'] ); } $update->doUpdate(); - } else { - DeferredUpdates::addUpdate( $update, $options['defer'] ); } + } else { + $cacheTime = $this->getCanonicalParserOutput()->getCacheTime(); + // Bundle all of the data updates into a single deferred update wrapper so that + // any failure will cause at most one refreshLinks job to be enqueued by + // DeferredUpdates::doUpdates(). This is hard to do when there are many separate + // updates that are not defined as being related. + $update = new RefreshSecondaryDataUpdate( + $this->wikiPage, + $updates, + $options, + $cacheTime, + $this->loadbalancerFactory->getLocalDomainID() + ); + $update->setRevision( $legacyRevision ); + $update->setTriggeringUser( $triggeringUser ); + DeferredUpdates::addUpdate( $update, $options['defer'] ); } } diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index bb2d3f780c..a051d83aef 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -591,7 +591,7 @@ class DatabaseOracle extends Database { } } - public function upsert( $table, array $rows, array $uniqueIndexes, array $set, + public function upsert( $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ ) { if ( $rows === [] ) { diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php index e50f855edb..cb1a69dfd4 100644 --- a/includes/db/MWLBFactory.php +++ b/includes/db/MWLBFactory.php @@ -54,7 +54,9 @@ abstract class MWLBFactory { $mainConfig->get( 'DBmwschema' ), $mainConfig->get( 'DBprefix' ) ), - 'profiler' => Profiler::instance(), + 'profiler' => function ( $section ) { + return Profiler::instance()->scopedProfileIn( $section ); + }, 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ), 'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ), diff --git a/includes/deferred/CdnCacheUpdate.php b/includes/deferred/CdnCacheUpdate.php index 5329ca7e8f..6f961e8b71 100644 --- a/includes/deferred/CdnCacheUpdate.php +++ b/includes/deferred/CdnCacheUpdate.php @@ -62,15 +62,6 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate { return new CdnCacheUpdate( $urlArr ); } - /** - * @param Title $title - * @return CdnCacheUpdate - * @deprecated since 1.27 - */ - public static function newSimplePurge( Title $title ) { - return new CdnCacheUpdate( $title->getCdnUrls() ); - } - /** * Purges the list of URLs passed to the constructor. */ diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index 7a31e26253..101a1e296b 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -32,7 +32,7 @@ use Wikimedia\ScopedCallback; * * See docs/deferred.txt */ -class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { +class LinksUpdate extends DataUpdate { // @todo make members protected, but make sure extensions don't break /** @var int Page ID of the article linked from */ @@ -1187,39 +1187,4 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate { return $this->db; } - - public function getAsJobSpecification() { - if ( $this->user ) { - $userInfo = [ - 'userId' => $this->user->getId(), - 'userName' => $this->user->getName(), - ]; - } else { - $userInfo = false; - } - - if ( $this->mRevision ) { - $triggeringRevisionId = $this->mRevision->getId(); - } else { - $triggeringRevisionId = false; - } - - return [ - 'wiki' => WikiMap::getWikiIdFromDbDomain( $this->getDB()->getDomainID() ), - 'job' => new JobSpecification( - 'refreshLinksPrioritized', - [ - // Reuse the parser cache if it was saved - 'rootJobTimestamp' => $this->mParserOutput->getCacheTime(), - 'useRecursiveLinksUpdate' => $this->mRecursive, - 'triggeringUser' => $userInfo, - 'triggeringRevisionId' => $triggeringRevisionId, - 'causeAction' => $this->getCauseAction(), - 'causeAgent' => $this->getCauseAgent() - ], - [ 'removeDuplicates' => true ], - $this->getTitle() - ) - ]; - } } diff --git a/includes/deferred/RefreshSecondaryDataUpdate.php b/includes/deferred/RefreshSecondaryDataUpdate.php new file mode 100644 index 0000000000..8086a7050d --- /dev/null +++ b/includes/deferred/RefreshSecondaryDataUpdate.php @@ -0,0 +1,117 @@ +page = $page; + $this->updates = $updates; + $this->causeAction = $options['causeAction'] ?? 'unknown'; + $this->causeAgent = $options['causeAgent'] ?? 'unknown'; + $this->recursive = !empty( $options['recursive'] ); + $this->cacheTimestamp = $cacheTime; + $this->domain = $domain; + } + + public function doUpdate() { + foreach ( $this->updates as $update ) { + $update->doUpdate(); + } + } + + /** + * Set the revision corresponding to this LinksUpdate + * @param Revision $revision + */ + public function setRevision( Revision $revision ) { + $this->revision = $revision; + } + + /** + * Set the User who triggered this LinksUpdate + * @param User $user + */ + public function setTriggeringUser( User $user ) { + $this->user = $user; + } + + public function getAsJobSpecification() { + return [ + 'wiki' => WikiMap::getWikiIdFromDomain( $this->domain ), + 'job' => new JobSpecification( + 'refreshLinksPrioritized', + [ + // Reuse the parser cache if it was saved + 'rootJobTimestamp' => $this->cacheTimestamp, + 'useRecursiveLinksUpdate' => $this->recursive, + 'triggeringUser' => $this->user + ? [ + 'userId' => $this->user->getId(), + 'userName' => $this->user->getName() + ] + : false, + 'triggeringRevisionId' => $this->revision ? $this->revision->getId() : false, + 'causeAction' => $this->getCauseAction(), + 'causeAgent' => $this->getCauseAgent() + ], + [ 'removeDuplicates' => true ], + $this->page->getTitle() + ) + ]; + } +} diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index cbf9bff93f..a09160827b 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -189,7 +189,9 @@ class FileBackendGroup { 'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(), 'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ), 'logger' => LoggerFactory::getInstance( 'FileOperation' ), - 'profiler' => Profiler::instance() + 'profiler' => function ( $section ) { + return Profiler::instance()->scopedProfileIn( $section ); + } ]; $config['lockManager'] = LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] ); diff --git a/includes/libs/filebackend/FileBackend.php b/includes/libs/filebackend/FileBackend.php index a80b6d0c06..19373eaef9 100644 --- a/includes/libs/filebackend/FileBackend.php +++ b/includes/libs/filebackend/FileBackend.php @@ -114,7 +114,7 @@ abstract class FileBackend implements LoggerAwareInterface { protected $fileJournal; /** @var LoggerInterface */ protected $logger; - /** @var object|string Class name or object With profileIn/profileOut methods */ + /** @var callable|null */ protected $profiler; /** @var callable */ @@ -156,7 +156,8 @@ abstract class FileBackend implements LoggerAwareInterface { * - obResetFunc : alternative callback to clear the output buffer * - streamMimeFunc : alternative method to determine the content type from the path * - logger : Optional PSR logger object. - * - profiler : Optional class name or object With profileIn/profileOut methods. + * - profiler : Optional callback that takes a section name argument and returns + * a ScopedCallback instance that ends the profile section in its destructor. * @throws InvalidArgumentException */ public function __construct( array $config ) { @@ -187,6 +188,9 @@ abstract class FileBackend implements LoggerAwareInterface { $this->statusWrapper = $config['statusWrapper'] ?? null; $this->profiler = $config['profiler'] ?? null; + if ( !is_callable( $this->profiler ) ) { + $this->profiler = null; + } $this->logger = $config['logger'] ?? new \Psr\Log\NullLogger(); $this->statusWrapper = $config['statusWrapper'] ?? null; $this->tmpDirectory = $config['tmpDirectory'] ?? null; @@ -1599,12 +1603,7 @@ abstract class FileBackend implements LoggerAwareInterface { * @return ScopedCallback|null */ protected function scopedProfileSection( $section ) { - if ( $this->profiler ) { - call_user_func( [ $this->profiler, 'profileIn' ], $section ); - return new ScopedCallback( [ $this->profiler, 'profileOut' ], [ $section ] ); - } - - return null; + return $this->profiler ? ( $this->profiler )( $section ) : null; } protected function resetOutputBuffer() { diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 1fedfaf616..847a1eb069 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -97,6 +97,14 @@ class APCBagOStuff extends BagOStuff { return true; } + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + return apc_add( + $key . self::KEY_SUFFIX, + $this->setSerialize( $value ), + $exptime + ); + } + protected function setSerialize( $value ) { if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) { $value = serialize( $value ); diff --git a/includes/libs/objectcache/APCUBagOStuff.php b/includes/libs/objectcache/APCUBagOStuff.php index fb43d4ddb3..d5f1edc163 100644 --- a/includes/libs/objectcache/APCUBagOStuff.php +++ b/includes/libs/objectcache/APCUBagOStuff.php @@ -55,6 +55,14 @@ class APCUBagOStuff extends APCBagOStuff { return true; } + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + return apcu_add( + $key . self::KEY_SUFFIX, + $this->setSerialize( $value ), + $exptime + ); + } + public function delete( $key, $flags = 0 ) { apcu_delete( $key . self::KEY_SUFFIX ); diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index 2fb978d6a6..c439f9ba34 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -91,8 +91,8 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { 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 = 1; // synchronously write to all locations for replicated stores - const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache + 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 /** * $params include: @@ -144,7 +144,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { * @param string $key * @param int $ttl Time-to-live (seconds) * @param callable $callback Callback that derives the new value - * @param int $flags Bitfield of BagOStuff::READ_* constants [optional] + * @param int $flags Bitfield of BagOStuff::READ_* or BagOStuff::WRITE_* constants [optional] * @return mixed The cached value if found or the result of $callback otherwise * @since 1.27 */ @@ -157,7 +157,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { } $value = call_user_func( $callback ); if ( $value !== false ) { - $this->set( $key, $value, $ttl ); + $this->set( $key, $value, $ttl, $flags ); } } @@ -288,9 +288,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { * @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 */ - protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) { + protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { do { $this->clearLastError(); $reportDupes = $this->reportDupes; @@ -316,10 +317,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { $success = true; // do nothing } elseif ( $currentValue === false ) { // Try to create the key, failing if it gets created in the meantime - $success = $this->add( $key, $value, $exptime ); + $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 ); + $success = $this->cas( $casToken, $key, $value, $exptime, $flags ); } if ( $this->getLastError() ) { $this->logger->warning( @@ -341,10 +342,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { * @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 * @throws Exception */ - protected function cas( $casToken, $key, $value, $exptime = 0 ) { + protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { if ( !$this->lock( $key, 0 ) ) { return false; // non-blocking } @@ -352,7 +354,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { $curCasToken = null; // passed by reference $this->getWithToken( $key, $curCasToken, self::READ_LATEST ); if ( $casToken === $curCasToken ) { - $success = $this->set( $key, $value, $exptime ); + $success = $this->set( $key, $value, $exptime, $flags ); } else { $this->logger->info( __METHOD__ . ' failed due to race condition for {key}.', @@ -424,13 +426,14 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { * * @param string $key * @param int $expiry + * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) * @return bool Success Returns false if there is no key * @since 1.28 */ - public function changeTTL( $key, $expiry = 0 ) { + public function changeTTL( $key, $expiry = 0, $flags = 0 ) { $value = $this->get( $key ); - return ( $value === false ) ? false : $this->set( $key, $value, $expiry ); + return ( $value === false ) ? false : $this->set( $key, $value, $expiry, $flags ); } /** @@ -461,7 +464,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { function () use ( $key, $expiry, $fname ) { $this->clearLastError(); if ( $this->add( "{$key}:lock", 1, $expiry ) ) { - return true; // locked! + return WaitConditionLoop::CONDITION_REACHED; // locked! } elseif ( $this->getLastError() ) { $this->logger->warning( $fname . ' failed due to I/O error for {key}.', @@ -569,51 +572,66 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { /** * Get an associative array containing the item for each of the keys that have items. - * @param array $keys List of strings + * @param string[] $keys List of keys * @param int $flags Bitfield; supports READ_LATEST [optional] * @return array */ public function getMulti( array $keys, $flags = 0 ) { $res = []; foreach ( $keys as $key ) { - $val = $this->get( $key ); + $val = $this->get( $key, $flags ); if ( $val !== false ) { $res[$key] = $val; } } + return $res; } /** - * Batch insertion - * @param array $data $key => $value assoc array + * Batch insertion/replace + * @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 ) { + public function setMulti( array $data, $exptime = 0, $flags = 0 ) { $res = true; foreach ( $data as $key => $value ) { - if ( !$this->set( $key, $value, $exptime ) ) { + if ( !$this->set( $key, $value, $exptime, $flags ) ) { $res = false; } } + + return $res; + } + + /** + * Batch deletion + * @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 ) { + $res = true; + foreach ( $keys as $key ) { + $res = $this->delete( $key, $flags ) && $res; + } + return $res; } /** + * Insertion * @param string $key * @param mixed $value * @param int $exptime + * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) * @return bool Success */ - public function add( $key, $value, $exptime = 0 ) { - // @note: avoid lock() here since that method uses *this* method by default - if ( $this->get( $key ) === false ) { - return $this->set( $key, $value, $exptime ); - } - return false; // key already set - } + abstract public function add( $key, $value, $exptime = 0, $flags = 0 ); /** * Increase stored value of $key by $value while preserving its TTL @@ -625,7 +643,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { if ( !$this->lock( $key, 1 ) ) { return false; } - $n = $this->get( $key ); + $n = $this->get( $key, self::READ_LATEST ); if ( $this->isInteger( $n ) ) { // key exists? $n += intval( $value ); $this->set( $key, max( 0, $n ) ); // exptime? diff --git a/includes/libs/objectcache/CachedBagOStuff.php b/includes/libs/objectcache/CachedBagOStuff.php index 25fcdb0e2c..95b12b4047 100644 --- a/includes/libs/objectcache/CachedBagOStuff.php +++ b/includes/libs/objectcache/CachedBagOStuff.php @@ -101,6 +101,14 @@ class CachedBagOStuff extends HashBagOStuff { // 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 lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) { return $this->backend->lock( $key, $timeout, $expiry, $rclass ); } diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php index 3bf58df892..9300dc274d 100644 --- a/includes/libs/objectcache/EmptyBagOStuff.php +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -31,7 +31,7 @@ class EmptyBagOStuff extends BagOStuff { return false; } - public function add( $key, $value, $exp = 0 ) { + public function add( $key, $value, $exp = 0, $flags = 0 ) { return true; } diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index 7d074fa560..f88f5671ad 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -106,6 +106,14 @@ class HashBagOStuff extends BagOStuff { return true; } + 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 delete( $key, $flags = 0 ) { unset( $this->bag[$key] ); diff --git a/includes/libs/objectcache/MemcachedBagOStuff.php b/includes/libs/objectcache/MemcachedBagOStuff.php index 47e04d0f03..06e90a0b4e 100644 --- a/includes/libs/objectcache/MemcachedBagOStuff.php +++ b/includes/libs/objectcache/MemcachedBagOStuff.php @@ -65,7 +65,7 @@ class MemcachedBagOStuff extends BagOStuff { $this->fixExpiry( $exptime ) ); } - protected function cas( $casToken, $key, $value, $exptime = 0 ) { + protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ), $value, $this->fixExpiry( $exptime ) ); } @@ -74,7 +74,7 @@ class MemcachedBagOStuff extends BagOStuff { return $this->client->delete( $this->validateKeyEncoding( $key ) ); } - public function add( $key, $value, $exptime = 0 ) { + public function add( $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->add( $this->validateKeyEncoding( $key ), $value, $this->fixExpiry( $exptime ) ); } @@ -83,7 +83,7 @@ class MemcachedBagOStuff extends BagOStuff { return $this->mergeViaCas( $key, $callback, $exptime, $attempts ); } - public function changeTTL( $key, $exptime = 0 ) { + public function changeTTL( $key, $exptime = 0, $flags = 0 ) { return $this->client->touch( $this->validateKeyEncoding( $key ), $this->fixExpiry( $exptime ) ); } diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php index a6646bcd89..62a57b6c36 100644 --- a/includes/libs/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPeclBagOStuff.php @@ -159,7 +159,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { public function set( $key, $value, $exptime = 0, $flags = 0 ) { $this->debugLog( "set($key)" ); - $result = parent::set( $key, $value, $exptime ); + $result = parent::set( $key, $value, $exptime, $flags = 0 ); if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) { // "Not stored" is always used as the mcrouter response with AllAsyncRoute return true; @@ -167,9 +167,9 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( $key, $result ); } - protected function cas( $casToken, $key, $value, $exptime = 0 ) { + protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { $this->debugLog( "cas($key)" ); - return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) ); + return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime, $flags ) ); } public function delete( $key, $flags = 0 ) { @@ -182,7 +182,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( $key, $result ); } - public function add( $key, $value, $exptime = 0 ) { + public function add( $key, $value, $exptime = 0, $flags = 0 ) { $this->debugLog( "add($key)" ); return $this->checkResult( $key, parent::add( $key, $value, $exptime ) ); } @@ -248,12 +248,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( false, $result ); } - /** - * @param array $data - * @param int $exptime - * @return bool - */ - public function setMulti( array $data, $exptime = 0 ) { + public function setMulti( array $data, $exptime = 0, $flags = 0 ) { $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' ); foreach ( array_keys( $data ) as $key ) { $this->validateKeyEncoding( $key ); @@ -262,7 +257,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $this->checkResult( false, $result ); } - public function changeTTL( $key, $expiry = 0 ) { + public function changeTTL( $key, $expiry = 0, $flags = 0 ) { $this->debugLog( "touch($key)" ); $result = $this->client->touch( $key, $expiry ); return $this->checkResult( $key, $result ); diff --git a/includes/libs/objectcache/MultiWriteBagOStuff.php b/includes/libs/objectcache/MultiWriteBagOStuff.php index f549876554..5cf9de4cc8 100644 --- a/includes/libs/objectcache/MultiWriteBagOStuff.php +++ b/includes/libs/objectcache/MultiWriteBagOStuff.php @@ -143,7 +143,7 @@ class MultiWriteBagOStuff extends BagOStuff { return $this->doWrite( $this->cacheIndexes, $this->asyncWrites, 'delete', $key ); } - public function add( $key, $value, $exptime = 0 ) { + public function add( $key, $value, $exptime = 0, $flags = 0 ) { // Try the write to the top-tier cache $ok = $this->doWrite( [ 0 ], $this->asyncWrites, 'add', $key, $value, $exptime ); if ( $ok ) { diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php index b0b82d86ed..135556adb5 100644 --- a/includes/libs/objectcache/RESTBagOStuff.php +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -138,6 +138,14 @@ class RESTBagOStuff extends BagOStuff { return $this->handleError( "Failed to store $key", $rcode, $rerr ); } + 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 delete( $key, $flags = 0 ) { // @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM) $req = [ diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index 3926604f30..f64fe7e780 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -189,12 +189,7 @@ class RedisBagOStuff extends BagOStuff { return $result; } - /** - * @param array $data - * @param int $expiry - * @return bool - */ - public function setMulti( array $data, $expiry = 0 ) { + public function setMulti( array $data, $expiry = 0, $flags = 0 ) { $batches = []; $conns = []; foreach ( $data as $key => $value ) { @@ -238,7 +233,46 @@ class RedisBagOStuff extends BagOStuff { return $result; } - public function add( $key, $value, $expiry = 0 ) { + public function deleteMulti( array $keys, $flags = 0 ) { + $batches = []; + $conns = []; + foreach ( $keys as $key ) { + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + continue; + } + $conns[$server] = $conn; + $batches[$server][] = $key; + } + + $result = true; + foreach ( $batches as $server => $batchKeys ) { + $conn = $conns[$server]; + try { + $conn->multi( Redis::PIPELINE ); + foreach ( $batchKeys as $key ) { + $conn->delete( $key ); + } + $batchResult = $conn->exec(); + if ( $batchResult === false ) { + $this->debug( "deleteMulti request to $server failed" ); + continue; + } + foreach ( $batchResult as $value ) { + if ( $value === false ) { + $result = false; + } + } + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + $result = false; + } + } + + return $result; + } + + public function add( $key, $value, $expiry = 0, $flags = 0 ) { list( $server, $conn ) = $this->getConnection( $key ); if ( !$conn ) { return false; @@ -299,7 +333,7 @@ class RedisBagOStuff extends BagOStuff { return $result; } - public function changeTTL( $key, $expiry = 0 ) { + public function changeTTL( $key, $expiry = 0, $flags = 0 ) { list( $server, $conn ) = $this->getConnection( $key ); if ( !$conn ) { return false; diff --git a/includes/libs/objectcache/ReplicatedBagOStuff.php b/includes/libs/objectcache/ReplicatedBagOStuff.php index e2b9a52df1..14e2fefbd9 100644 --- a/includes/libs/objectcache/ReplicatedBagOStuff.php +++ b/includes/libs/objectcache/ReplicatedBagOStuff.php @@ -90,12 +90,20 @@ class ReplicatedBagOStuff extends BagOStuff { return $this->writeStore->set( $key, $value, $exptime, $flags ); } + public function setMulti( array $keys, $exptime = 0, $flags = 0 ) { + return $this->writeStore->setMulti( $keys, $exptime, $flags ); + } + public function delete( $key, $flags = 0 ) { return $this->writeStore->delete( $key, $flags ); } - public function add( $key, $value, $exptime = 0 ) { - return $this->writeStore->add( $key, $value, $exptime ); + public function deleteMulti( array $keys, $flags = 0 ) { + return $this->writeStore->deleteMulti( $keys, $flags ); + } + + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + return $this->writeStore->add( $key, $value, $exptime, $flags ); } public function incr( $key, $value = 1 ) { diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index 6b0bec0025..cae0280128 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -40,9 +40,13 @@ class WinCacheBagOStuff extends BagOStuff { public function set( $key, $value, $expire = 0, $flags = 0 ) { $result = wincache_ucache_set( $key, serialize( $value ), $expire ); - /* wincache_ucache_set returns an empty array on success if $value - * was an array, bool otherwise */ - return ( is_array( $result ) && $result === [] ) || $result; + return ( $result === [] || $result === true ); + } + + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + $result = wincache_ucache_add( $key, serialize( $value ), $exptime ); + + return ( $result === [] || $result === true ); } public function delete( $key, $flags = 0 ) { diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index ab70fc80cc..4fdac81e66 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -417,7 +417,7 @@ class DBConnRef implements IDatabase { } public function upsert( - $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 49b2792210..224bcf29d0 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -260,7 +260,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var int[] Prior flags member variable values */ private $priorFlags = []; - /** @var mixed Class name or object With profileIn/profileOut methods */ + /** @var callable|null */ protected $profiler; /** @var TransactionProfiler */ protected $trxProfiler; @@ -308,7 +308,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->srvCache = $params['srvCache'] ?? new HashBagOStuff(); - $this->profiler = $params['profiler']; + $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null; $this->trxProfiler = $params['trxProfiler']; $this->connLogger = $params['connLogger']; $this->queryLogger = $params['queryLogger']; @@ -408,9 +408,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * used to adjust lock timeouts or encoding modes and the like. * - connLogger: Optional PSR-3 logger interface instance. * - queryLogger: Optional PSR-3 logger interface instance. - * - profiler: Optional class name or object with profileIn()/profileOut() methods. - * These will be called in query(), using a simplified version of the SQL that also - * includes the agent as a SQL comment. + * - profiler : Optional callback that takes a section name argument and returns + * a ScopedCallback instance that ends the profile section in its destructor. + * These will be called in query(), using a simplified version of the SQL that + * also includes the agent as a SQL comment. * - trxProfiler: Optional TransactionProfiler instance. * - errorLogger: Optional callback that takes an Exception and logs it. * - deprecationLogger: Optional callback that takes a string and logs it. @@ -991,16 +992,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * Make sure isOpen() returns true as a sanity check + * Make sure there is an open connection handle (alive or not) as a sanity check + * + * This guards against fatal errors to the binding handle not being defined + * in cases where open() was never called or close() was already called * * @throws DBUnexpectedError */ - protected function assertOpen() { + protected function assertHasConnectionHandle() { if ( !$this->isOpen() ) { throw new DBUnexpectedError( $this, "DB connection was already closed." ); } } + /** + * Make sure that this server is not marked as a replica nor read-only as a sanity check + * + * @throws DBUnexpectedError + */ + protected function assertIsWritableMaster() { + if ( $this->getLBInfo( 'replica' ) === true ) { + throw new DBUnexpectedError( + $this, + 'Write operations are not allowed on replica database connections.' + ); + } + $reason = $this->getReadOnlyReason(); + if ( $reason !== false ) { + throw new DBReadOnlyError( $this, "Database is read-only: $reason" ); + } + } + /** * Closes underlying database connection * @since 1.20 @@ -1144,99 +1166,72 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { $this->assertTransactionStatus( $sql, $fname ); + $this->assertHasConnectionHandle(); - # Avoid fatals if close() was called - $this->assertOpen(); - + $priorTransaction = $this->trxLevel; $priorWritesPending = $this->writesOrCallbacksPending(); $this->lastQuery = $sql; - $isWrite = $this->isWriteQuery( $sql ); - if ( $isWrite ) { - $isNonTempWrite = !$this->registerTempTableOperation( $sql ); - } else { - $isNonTempWrite = false; - } - - if ( $isWrite ) { - if ( $this->getLBInfo( 'replica' ) === true ) { - throw new DBError( - $this, - 'Write operations are not allowed on replica database connections.' - ); - } + if ( $this->isWriteQuery( $sql ) ) { # In theory, non-persistent writes are allowed in read-only mode, but due to things # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway... - $reason = $this->getReadOnlyReason(); - if ( $reason !== false ) { - throw new DBReadOnlyError( $this, "Database is read-only: $reason" ); - } - # Set a flag indicating that writes have been done - $this->lastWriteTime = microtime( true ); + $this->assertIsWritableMaster(); + # Avoid treating temporary table operations as meaningful "writes" + $isEffectiveWrite = !$this->registerTempTableOperation( $sql ); + } else { + $isEffectiveWrite = false; } # Add trace comment to the begin of the sql string, right after the operator. # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598) $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 ); - # Start implicit transactions that wrap the request if DBO_TRX is enabled - if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX ) - && $this->isTransactableQuery( $sql ) - ) { - $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL ); - $this->trxAutomatic = true; - } - - # Keep track of whether the transaction has write queries pending - if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) { - $this->trxDoneWrites = true; - $this->trxProfiler->transactionWritingIn( - $this->server, $this->getDomainID(), $this->trxShortId ); - } - - if ( $this->getFlag( self::DBO_DEBUG ) ) { - $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" ); - } - # Send the query to the server and fetch any corresponding errors - $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname ); + $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ); $lastError = $this->lastError(); $lastErrno = $this->lastErrno(); - # Try reconnecting if the connection was lost + $recoverableSR = false; // recoverable statement rollback? + $recoverableCL = false; // recoverable connection loss? + if ( $ret === false && $this->wasConnectionLoss() ) { - # Check if any meaningful session state was lost - $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending ); + # Check if no meaningful session state was lost + $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending ); # Update session state tracking and try to restore the connection $reconnected = $this->replaceLostConnection( __METHOD__ ); # Silently resend the query to the server if it is safe and possible - if ( $reconnected && $recoverable ) { - $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname ); + if ( $recoverableCL && $reconnected ) { + $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ); $lastError = $this->lastError(); $lastErrno = $this->lastErrno(); if ( $ret === false && $this->wasConnectionLoss() ) { # Query probably causes disconnects; reconnect and do not re-run it $this->replaceLostConnection( __METHOD__ ); + } else { + $recoverableCL = false; // connection does not need recovering + $recoverableSR = $this->wasKnownStatementRollbackError(); } } + } else { + $recoverableSR = $this->wasKnownStatementRollbackError(); } if ( $ret === false ) { - if ( $this->trxLevel ) { - if ( $this->wasKnownStatementRollbackError() ) { + if ( $priorTransaction ) { + if ( $recoverableSR ) { # We're ignoring an error that caused just the current query to be aborted. # But log the cause so we can log a deprecation notice if a caller actually # does ignore it. $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ]; - } else { + } elseif ( !$recoverableCL ) { # Either the query was aborted or all queries after BEGIN where aborted. # In the first case, the only options going forward are (a) ROLLBACK, or # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only # option is ROLLBACK, since the snapshots would have been released. $this->trxStatus = self::STATUS_TRX_ERROR; $this->trxStatusCause = - $this->makeQueryException( $lastError, $lastErrno, $sql, $fname ); + $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname ); $tempIgnore = false; // cannot recover $this->trxStatusIgnoredCause = null; } @@ -1253,12 +1248,28 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @param string $sql Original SQL query * @param string $commentedSql SQL query with debugging/trace comment - * @param bool $isWrite Whether the query is a (non-temporary) write operation + * @param bool $isEffectiveWrite Whether the query is a (non-temporary table) write * @param string $fname Name of the calling function * @return bool|ResultWrapper True for a successful write query, ResultWrapper * object for a successful read query, or false on failure */ - private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) { + private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) { + $this->beginIfImplied( $sql, $fname ); + + # Keep track of whether the transaction has write queries pending + if ( $isEffectiveWrite ) { + $this->lastWriteTime = microtime( true ); + if ( $this->trxLevel && !$this->trxDoneWrites ) { + $this->trxDoneWrites = true; + $this->trxProfiler->transactionWritingIn( + $this->server, $this->getDomainID(), $this->trxShortId ); + } + } + + if ( $this->getFlag( self::DBO_DEBUG ) ) { + $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" ); + } + $isMaster = !is_null( $this->getLBInfo( 'master' ) ); # generalizeSQL() will probably cut down the query to reasonable # logging size most of the time. The substr is really just a sanity check. @@ -1272,22 +1283,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : ""; $startTime = microtime( true ); - if ( $this->profiler ) { - $this->profiler->profileIn( $queryProf ); - } + $ps = $this->profiler ? ( $this->profiler )( $queryProf ) : null; $this->affectedRowCount = null; $ret = $this->doQuery( $commentedSql ); $this->affectedRowCount = $this->affectedRows(); - if ( $this->profiler ) { - $this->profiler->profileOut( $queryProf ); - } + unset( $ps ); // profile out (if set) $queryRuntime = max( microtime( true ) - $startTime, 0.0 ); - unset( $queryProfSection ); // profile out (if set) - if ( $ret !== false ) { $this->lastPing = $startTime; - if ( $isWrite && $this->trxLevel ) { + if ( $isEffectiveWrite && $this->trxLevel ) { $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() ); $this->trxWriteCallers[] = $fname; } @@ -1300,8 +1305,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxProfiler->recordQueryCompletion( $queryProf, $startTime, - $isWrite, - $isWrite ? $this->affectedRows() : $this->numRows( $ret ) + $isEffectiveWrite, + $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret ) ); $this->queryLogger->debug( $sql, [ 'method' => $fname, @@ -1312,6 +1317,23 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $ret; } + /** + * Start an implicit transaction if DBO_TRX is enabled and no transaction is active + * + * @param string $sql + * @param string $fname + */ + private function beginIfImplied( $sql, $fname ) { + if ( + !$this->trxLevel && + $this->getFlag( self::DBO_TRX ) && + $this->isTransactableQuery( $sql ) + ) { + $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL ); + $this->trxAutomatic = true; + } + } + /** * Update the estimated run-time of a query, not counting large row lock times * @@ -1391,8 +1413,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * Determine whether or not it is safe to retry queries after a database - * connection is lost + * Determine whether it is safe to retry queries after a database connection is lost * * @param string $sql SQL query * @param bool $priorWritesPending Whether there is a transaction open with @@ -1441,6 +1462,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * Clean things up after transaction loss */ private function handleTransactionLoss() { + if ( $this->trxDoneWrites ) { + $this->trxProfiler->transactionWritingOut( + $this->server, + $this->getDomainID(), + $this->trxShortId, + $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ), + $this->trxWriteAffectedRows + ); + } $this->trxLevel = 0; $this->trxAtomicCounter = 0; $this->trxIdleCallbacks = []; // T67263; transaction already lost @@ -1489,7 +1519,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $tempIgnore ) { $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" ); } else { - $exception = $this->makeQueryException( $error, $errno, $sql, $fname ); + $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname ); throw $exception; } @@ -1502,7 +1532,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param string $fname * @return DBError */ - private function makeQueryException( $error, $errno, $sql, $fname ) { + private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) { $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); $this->queryLogger->error( "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}", @@ -1512,6 +1542,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware 'error' => $error, 'sql1line' => $sql1line, 'fname' => $fname, + 'trace' => ( new RuntimeException() )->getTraceAsString() ] ) ); $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" ); @@ -2782,6 +2813,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return; } + $uniqueIndexes = (array)$uniqueIndexes; // Single row case if ( !is_array( reset( $rows ) ) ) { $rows = [ $rows ]; @@ -2861,13 +2893,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->query( $sql, $fname ); } - public function upsert( $table, array $rows, array $uniqueIndexes, array $set, + public function upsert( $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ ) { if ( $rows === [] ) { return true; // nothing to do } + $uniqueIndexes = (array)$uniqueIndexes; if ( !is_array( reset( $rows ) ) ) { $rows = [ $rows ]; } @@ -3286,7 +3319,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * @return bool Whether it is safe to assume the given error only caused statement rollback + * @return bool Whether it is known that the last query error only caused statement rollback * @note This is for backwards compatibility for callers catching DBError exceptions in * order to ignore problems like duplicate key errors or foriegn key violations * @since 1.31 @@ -3847,8 +3880,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware throw new DBUnexpectedError( $this, $msg ); } - // Avoid fatals if close() was called - $this->assertOpen(); + $this->assertHasConnectionHandle(); $this->doBegin( $fname ); $this->trxStatus = self::STATUS_TRX_OK; @@ -3924,8 +3956,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } - // Avoid fatals if close() was called - $this->assertOpen(); + $this->assertHasConnectionHandle(); $this->runOnTransactionPreCommitCallbacks(); @@ -3977,8 +4008,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } if ( $trxActive ) { - // Avoid fatals if close() was called - $this->assertOpen(); + $this->assertHasConnectionHandle(); $this->doRollback( $fname ); $this->trxStatus = self::STATUS_TRX_NONE; diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 62110ef45a..7fbd34d02b 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -489,12 +489,6 @@ abstract class DatabaseMysqlBase extends Database { return $errno == 2062; } - /** - * @param string $table - * @param array $uniqueIndexes - * @param array $rows - * @param string $fname - */ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { $this->nativeReplace( $table, $rows, $fname ); } @@ -1326,16 +1320,8 @@ abstract class DatabaseMysqlBase extends Database { $this->query( $sql, $fname ); } - /** - * @param string $table - * @param array $rows - * @param array $uniqueIndexes - * @param array $set - * @param string $fname - * @return bool - */ - public function upsert( $table, array $rows, array $uniqueIndexes, - array $set, $fname = __METHOD__ + public function upsert( + $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ ) { if ( $rows === [] ) { return true; // nothing to do diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 7d9eac1a57..3401541a17 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -1232,8 +1232,10 @@ interface IDatabase { * errors which wouldn't have occurred in MySQL. * * @param string $table The table to replace the row(s) in. - * @param array $uniqueIndexes Either a list of fields that define a unique index or - * an array of such lists if there are multiple unique indexes defined in the schema + * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following: + * a) the one unique field in the table (when no composite unique key exist) + * b) a list of all unique fields in the table (when no composite unique key exist) + * c) a list of all unique indexes in the table (each as a list of the indexed fields) * @param array $rows Can be either a single row to insert, or multiple rows, * in the same format as for IDatabase::insert() * @param string $fname Calling function name (use __METHOD__) for logs/profiling @@ -1267,8 +1269,10 @@ interface IDatabase { * * @param string $table Table name. This will be passed through Database::tableName(). * @param array $rows A single row or list of rows to insert - * @param array $uniqueIndexes Either a list of fields that define a unique index or - * an array of such lists if there are multiple unique indexes defined in the schema + * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following: + * a) the one unique field in the table (when no composite unique key exist) + * b) a list of all unique fields in the table (when no composite unique key exist) + * c) a list of all unique indexes in the table (each as a list of the indexed fields) * @param array $set An array of values to SET. For each array element, the * key gives the field name, and the value gives the data to set that * field to. The data will be quoted by IDatabase::addQuotes(). @@ -1279,7 +1283,7 @@ interface IDatabase { * @return bool Return true if no exception was thrown (deprecated since 1.33) */ public function upsert( - $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__ ); /** diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index eeb7355bc5..b2d61a8925 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -311,7 +311,11 @@ class SqlBagOStuff extends BagOStuff { return $values; } - public function setMulti( array $data, $expiry = 0 ) { + public function setMulti( array $data, $expiry = 0, $flags = 0 ) { + return $this->insertMulti( $data, $expiry, $flags, true ); + } + + private function insertMulti( array $data, $expiry, $flags, $replace ) { $keysByTable = []; foreach ( $data as $key => $value ) { list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); @@ -354,19 +358,22 @@ class SqlBagOStuff extends BagOStuff { } try { - $db->replace( - $tableName, - [ 'keyname' ], - $rows, - __METHOD__ - ); + if ( $replace ) { + $db->replace( $tableName, [ 'keyname' ], $rows, __METHOD__ ); + } else { + $db->insert( $tableName, $rows, __METHOD__, [ 'IGNORE' ] ); + $result = ( $db->affectedRows() > 0 && $result ); + } } catch ( DBError $e ) { $this->handleWriteError( $e, $db, $serverIndex ); $result = false; } } + } + if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { + $result = $this->waitForReplication() && $result; } return $result; @@ -374,14 +381,17 @@ class SqlBagOStuff extends BagOStuff { public function set( $key, $value, $exptime = 0, $flags = 0 ) { $ok = $this->setMulti( [ $key => $value ], $exptime ); - if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { - $ok = $this->waitForReplication() && $ok; - } return $ok; } - protected function cas( $casToken, $key, $value, $exptime = 0 ) { + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + $added = $this->insertMulti( [ $key => $value ], $exptime, $flags, false ); + + return $added; + } + + protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); $db = null; $silenceScope = $this->silenceTransactionProfiler(); @@ -423,26 +433,46 @@ class SqlBagOStuff extends BagOStuff { return (bool)$db->affectedRows(); } - public function delete( $key, $flags = 0 ) { - $ok = true; + public function deleteMulti( array $keys, $flags = 0 ) { + $keysByTable = []; + foreach ( $keys as $key ) { + list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); + $keysByTable[$serverIndex][$tableName][] = $key; + } - list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); - $db = null; + $result = true; $silenceScope = $this->silenceTransactionProfiler(); - try { - $db = $this->getDB( $serverIndex ); - $db->delete( - $tableName, - [ 'keyname' => $key ], - __METHOD__ ); - } catch ( DBError $e ) { - $this->handleWriteError( $e, $db, $serverIndex ); - $ok = false; + foreach ( $keysByTable as $serverIndex => $serverKeys ) { + $db = null; + try { + $db = $this->getDB( $serverIndex ); + } catch ( DBError $e ) { + $this->handleWriteError( $e, $db, $serverIndex ); + $result = false; + continue; + } + + foreach ( $serverKeys as $tableName => $tableKeys ) { + try { + $db->delete( $tableName, [ 'keyname' => $tableKeys ], __METHOD__ ); + } catch ( DBError $e ) { + $this->handleWriteError( $e, $db, $serverIndex ); + $result = false; + } + + } } + if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { - $ok = $this->waitForReplication() && $ok; + $result = $this->waitForReplication() && $result; } + return $result; + } + + public function delete( $key, $flags = 0 ) { + $ok = $this->deleteMulti( [ $key ], $flags ); + return $ok; } @@ -458,31 +488,34 @@ class SqlBagOStuff extends BagOStuff { [ 'value', 'exptime' ], [ 'keyname' => $key ], __METHOD__, - [ 'FOR UPDATE' ] ); + [ 'FOR UPDATE' ] + ); if ( $row === false ) { // Missing - - return null; + return false; } $db->delete( $tableName, [ 'keyname' => $key ], __METHOD__ ); if ( $this->isExpired( $db, $row->exptime ) ) { // Expired, do not reinsert - - return null; + return false; } $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) ); $newValue = $oldValue + $step; - $db->insert( $tableName, + $db->insert( + $tableName, [ 'keyname' => $key, 'value' => $db->encodeBlob( $this->serialize( $newValue ) ), 'exptime' => $row->exptime - ], __METHOD__, 'IGNORE' ); + ], + __METHOD__, + 'IGNORE' + ); if ( $db->affectedRows() == 0 ) { // Race condition. See T30611 - $newValue = null; + $newValue = false; } } catch ( DBError $e ) { $this->handleWriteError( $e, $db, $serverIndex ); @@ -493,7 +526,7 @@ class SqlBagOStuff extends BagOStuff { } public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { - $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts ); + $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags ); if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) { $ok = $this->waitForReplication() && $ok; } @@ -501,7 +534,7 @@ class SqlBagOStuff extends BagOStuff { return $ok; } - public function changeTTL( $key, $expiry = 0 ) { + public function changeTTL( $key, $expiry = 0, $flags = 0 ) { list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); $db = null; $silenceScope = $this->silenceTransactionProfiler(); diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index ce7ae1328a..8e8cd98c38 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -189,9 +189,8 @@ class ParserCache { } // Determine the options which affect this article - $casToken = null; $optionsKey = $this->mMemc->get( - $this->getOptionsKey( $article ), $casToken, BagOStuff::READ_VERIFIED ); + $this->getOptionsKey( $article ), BagOStuff::READ_VERIFIED ); if ( $optionsKey instanceof CacheTime ) { if ( $useOutdated < self::USE_EXPIRED && $optionsKey->expired( $article->getTouched() ) ) { $this->incrementStats( $article, "miss.expired" ); @@ -257,7 +256,7 @@ class ParserCache { $casToken = null; /** @var ParserOutput $value */ - $value = $this->mMemc->get( $parserOutputKey, $casToken, BagOStuff::READ_VERIFIED ); + $value = $this->mMemc->get( $parserOutputKey, BagOStuff::READ_VERIFIED ); if ( !$value ) { wfDebug( "ParserOutput cache miss.\n" ); $this->incrementStats( $article, "miss.absent" ); diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php index 455130cc05..554ca084bd 100644 --- a/includes/profiler/Profiler.php +++ b/includes/profiler/Profiler.php @@ -147,11 +147,12 @@ abstract class Profiler { } } - // Kept BC for now, remove when possible public function profileIn( $functionname ) { + wfDeprecated( __METHOD__, '1.33' ); } public function profileOut( $functionname ) { + wfDeprecated( __METHOD__, '1.33' ); } /** @@ -212,7 +213,7 @@ abstract class Profiler { } /** - * Log the data to some store or even the page output + * Log the data to the backing store for all ProfilerOutput instances that have one * * @since 1.25 */ @@ -225,27 +226,38 @@ abstract class Profiler { return; } - $outputs = $this->getOutputs(); - if ( !$outputs ) { - return; + $outputs = []; + foreach ( $this->getOutputs() as $output ) { + if ( !$output->logsToOutput() ) { + $outputs[] = $output; + } } - $stats = $this->getFunctionStats(); - foreach ( $outputs as $output ) { - $output->log( $stats ); + if ( $outputs ) { + $stats = $this->getFunctionStats(); + foreach ( $outputs as $output ) { + $output->log( $stats ); + } } } /** - * Output current data to the page output if configured to do so + * Log the data to the script/request output for all ProfilerOutput instances that do so * * @throws MWException * @since 1.26 */ public function logDataPageOutputOnly() { + $outputs = []; foreach ( $this->getOutputs() as $output ) { - if ( $output instanceof ProfilerOutputText ) { - $stats = $this->getFunctionStats(); + if ( $output->logsToOutput() ) { + $outputs[] = $output; + } + } + + if ( $outputs ) { + $stats = $this->getFunctionStats(); + foreach ( $outputs as $output ) { $output->log( $stats ); } } diff --git a/includes/profiler/output/ProfilerOutput.php b/includes/profiler/output/ProfilerOutput.php index 20b07801b4..fe27c046e7 100644 --- a/includes/profiler/output/ProfilerOutput.php +++ b/includes/profiler/output/ProfilerOutput.php @@ -47,6 +47,15 @@ abstract class ProfilerOutput { return true; } + /** + * Does log() just send the data to the request/script output? + * @return bool + * @since 1.33 + */ + public function logsToOutput() { + return false; + } + /** * Log MediaWiki-style profiling data * diff --git a/includes/profiler/output/ProfilerOutputText.php b/includes/profiler/output/ProfilerOutputText.php index e3184dbf8a..95b5ff95bc 100644 --- a/includes/profiler/output/ProfilerOutputText.php +++ b/includes/profiler/output/ProfilerOutputText.php @@ -35,6 +35,11 @@ class ProfilerOutputText extends ProfilerOutput { parent::__construct( $collector, $params ); $this->thresholdMs = $params['thresholdMs'] ?? 1.0; } + + public function logsToOutput() { + return true; + } + public function log( array $stats ) { if ( $this->collector->getTemplated() ) { $out = ''; diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 81e2f9e737..c0303b255c 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -45,6 +45,7 @@ class SpecialContributions extends IncludableSpecialPage { 'mediawiki.special', 'mediawiki.special.changeslist', ] ); + $out->addModules( 'mediawiki.special.recentchanges' ); $this->addHelpLink( 'Help:User contributions' ); $this->opts = []; @@ -607,6 +608,7 @@ class SpecialContributions extends IncludableSpecialPage { $labelNewbies . '
' . $labelUsername . ' ' . $input . ' ' ); + $hidden = $this->opts['namespace'] === '' ? ' mw-input-hidden' : ''; $namespaceSelection = Xml::tags( 'div', [], @@ -625,11 +627,11 @@ class SpecialContributions extends IncludableSpecialPage { ) . "\u{00A0}" . Html::rawElement( 'span', - [ 'class' => 'mw-input-with-label' ], + [ 'class' => 'mw-input-with-label' . $hidden ], Xml::checkLabel( $this->msg( 'invert' )->text(), 'nsInvert', - 'nsInvert', + 'nsinvert', $this->opts['nsInvert'], [ 'title' => $this->msg( 'tooltip-invert' )->text(), @@ -637,11 +639,11 @@ class SpecialContributions extends IncludableSpecialPage { ] ) . "\u{00A0}" ) . - Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' ], + Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' . $hidden ], Xml::checkLabel( $this->msg( 'namespace_association' )->text(), 'associated', - 'associated', + 'nsassociated', $this->opts['associated'], [ 'title' => $this->msg( 'tooltip-namespace_association' )->text(), diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index c052935eb4..6defc9de4f 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -67,6 +67,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $this->addHelpLink( 'Help:Watching pages' ); $output->addModuleStyles( [ 'mediawiki.special' ] ); $output->addModules( [ + 'mediawiki.special.recentchanges', 'mediawiki.special.watchlist', ] ); @@ -663,14 +664,15 @@ class SpecialWatchlist extends ChangesListSpecialPage { 'class' => 'namespaceselector', ] ) . "\n"; - $namespaceForm .= '' . Xml::checkLabel( + $hidden = $opts['namespace'] === '' ? ' mw-input-hidden' : ''; + $namespaceForm .= '' . Xml::checkLabel( $this->msg( 'invert' )->text(), 'invert', 'nsinvert', $opts['invert'], [ 'title' => $this->msg( 'tooltip-invert' )->text() ] ) . "\n"; - $namespaceForm .= '' . Xml::checkLabel( + $namespaceForm .= '' . Xml::checkLabel( $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated', diff --git a/includes/user/User.php b/includes/user/User.php index d6de0aa605..277731a0e5 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -4268,7 +4268,7 @@ class User implements IDBAccessObject, UserIdentity { Hooks::run( 'UserSaveSettings', [ $this ] ); $this->clearSharedCache(); - $this->getUserPage()->invalidateCache(); + $this->getUserPage()->purgeSquid(); } /** diff --git a/languages/i18n/az.json b/languages/i18n/az.json index 74f807b1be..e608d9f822 100644 --- a/languages/i18n/az.json +++ b/languages/i18n/az.json @@ -2106,6 +2106,7 @@ "markedaspatrollederror": "Yoxlanmadı", "markedaspatrollederror-noautopatrol": "Öz dəyişikliklərinizi yoxlayıb işarələyə bilməzsiniz.", "markedaspatrollednotify": "\"$1\" səhifəsindəki bu redaktə patrullanmış kimi işarələndi.", + "markedaspatrollederrornotify": "Patrullanma uğursuz oldu.", "patrol-log-page": "Patrul gündəliyi", "patrol-log-header": "Bu yoxlanmış dəyişikliklərin gündəliyidir.", "deletedrevision": "Köhnə versiyaları silindi $1.", diff --git a/languages/i18n/ckb.json b/languages/i18n/ckb.json index 62b598087f..bc596f20b2 100644 --- a/languages/i18n/ckb.json +++ b/languages/i18n/ckb.json @@ -2774,6 +2774,7 @@ "logentry-suppress-block": "$1 $3ی بۆ ماوەی $5 $6 بەربەست کرد", "logentry-suppress-reblock": "$1 ھەڵبژاردەکانی بەربەستنی $3ی گۆڕی بە ماوەی بەسەرچوونی $5 $6", "logentry-import-upload": "$1 {{GENDER:$2|بارکرد}} $3 بە بەکارھێنانی [[special:Import|بارکەر]]", + "logentry-import-interwiki-details": "$1 $3ی لە $5ەوە ھەناردە کرد ($4 بەسەرداچوونەوە)", "logentry-move-move": "$1 پەڕەی $3ی {{GENDER:$2|گواستەوە}} بۆ $4", "logentry-move-move-noredirect": "$1 پەڕەی $3ی بە بێ بەجێھشتنی ڕەوانەکەرێک {{GENDER:$2|گواستەوە}} بۆ $4", "logentry-move-move_redir": "$1 پەڕەی $3 {{GENDER:$2|گواستەوە}} بۆ $4 کە پێشتر ڕەوانەکەر بوو", diff --git a/languages/i18n/de.json b/languages/i18n/de.json index 1fe7738b07..c71c147a13 100644 --- a/languages/i18n/de.json +++ b/languages/i18n/de.json @@ -141,6 +141,7 @@ "tog-norollbackdiff": "Unterschiede nach dem Zurücksetzen nicht anzeigen", "tog-useeditwarning": "Warnen, sofern eine zur Bearbeitung geöffnete Seite verlassen wird, die nicht gespeicherte Änderungen enthält", "tog-prefershttps": "Immer eine sichere Verbindung benutzen, solange ich angemeldet bin", + "tog-showrollbackconfirmation": "Beim Klicken auf einen Zurücksetzen-Link eine Bestätigungsaufforderung anzeigen", "underline-always": "immer", "underline-never": "nie", "underline-default": "abhängig von der Benutzeroberfläche oder Browsereinstellung", diff --git a/languages/i18n/es.json b/languages/i18n/es.json index 25755e5cb7..c427f5adf1 100644 --- a/languages/i18n/es.json +++ b/languages/i18n/es.json @@ -184,7 +184,8 @@ "Pipino-pumuki", "Carlosmg.dg", "Mynor Archila", - "Jorge Ubilla" + "Jorge Ubilla", + "Marcelo9987" ] }, "tog-underline": "Enlaces a subrayar:", @@ -2645,6 +2646,7 @@ "ipb-sitewide": "En todo el sitio", "ipb-partial": "Parcial", "ipb-sitewide-help": "Todas las páginas en la Wiki y todas las acciones de contribución.", + "ipb-partial-help": "Páginas concretas o nombres.", "ipb-pages-label": "Páginas", "ipb-namespaces-label": "Espacios de nombres", "badipaddress": "La dirección IP no tiene el formato correcto.", @@ -3945,6 +3947,8 @@ "passwordpolicies-policy-maximalpasswordlength": "La contraseña no puede tener más de $1 {{PLURAL:$1|caracter|caracteres}}", "passwordpolicies-policy-passwordcannotbepopular": "La contraseña no puede {{PLURAL:$1|ser la contraseña más popular|encontrarse en la lista de $1 contraseñas populares}}", "passwordpolicies-policy-passwordnotinlargeblacklist": "La contraseña no puede estar en la lista de las 100.000 contraseñas más usadas.", + "passwordpolicies-policyflag-forcechange": "Sugerir cambio al iniciar sesion", + "passwordpolicies-policyflag-suggestchangeonlogin": "Sugerir cambio al iniciar sesion", "easydeflate-invaliddeflate": "El contenido proporcionado no esta comprimido correctamente", "unprotected-js": "Por razones de seguridad, JavaScript no se puede cargar desde páginas desprotegidas. Crea javascript solo en MediaWiki: espacio de nombres o como subpágina de usuario" } diff --git a/languages/i18n/fy.json b/languages/i18n/fy.json index 783728216b..46d1b9b31b 100644 --- a/languages/i18n/fy.json +++ b/languages/i18n/fy.json @@ -268,7 +268,7 @@ "feed-rss": "RSS", "red-link-title": "$1 (de side bestiet net)", "nstab-main": "Side", - "nstab-user": "Meidogger", + "nstab-user": "Meidoggerside", "nstab-media": "Mediaside", "nstab-special": "Bysûndere side", "nstab-project": "Projektside", @@ -280,8 +280,8 @@ "mainpage-nstab": "Haadside", "nosuchaction": "Unbekende aksje.", "nosuchactiontext": "De opdracht yn de URL is ûnjildich.\nMooglik hasto in typefout makke yn de URL of in ferkearde keppeling folge.\nIt soe likegoed in programmatuerflater fan {{SITENAME}} wêze kinne.", - "nosuchspecialpage": "Unbekende side", - "nospecialpagetext": "Jo hawwe in Wiki-side opfrege dy't net bekend is by it Wiki-programma.", + "nosuchspecialpage": "Gjin soksoarte bysûndere side", + "nospecialpagetext": "Jo hawwe in ûnjildige bysûndere side opfrege.\n\nIn list fan jildige bysûndere siden stiet op [[Special:SpecialPages|{{int:specialpages}}]].", "error": "Flater", "databaseerror": "Databankfout", "databaseerror-query": "Sykopdracht: $1", @@ -330,8 +330,8 @@ "logouttext": "Jo binne no ôfmeld.\n\nGuon siden kinne noch foar it ljocht komme, krekt as wiesto noch oanmeld. Asto de cache fan dyn webblêder leechhellest feroaret dat wer.", "welcomeuser": "Wolkom, $1!", "yourname": "Brûkersnamme:", - "userlogin-yourname": "Brûkersnamme", - "userlogin-yourname-ph": "Jou jo brûkersnamme", + "userlogin-yourname": "Meidochnamme", + "userlogin-yourname-ph": "Jou jo meidochnamme", "createacct-another-username-ph": "Jou jo brûkersnamme", "yourpassword": "Wachtwurd:", "userlogin-yourpassword": "Wachtwurd", @@ -376,7 +376,7 @@ "nocookiesnew": "De brûker is oanmakke mar net oanmeld. {{SITENAME}} brûkt cookies foar it oanmelden fan brûkers. Skeakelje dy yn en meld jo dan oan mei jo nije brûkersnamme en wachtwurd.", "nocookieslogin": "{{SITENAME}} brûkt cookies foar it oanmelden fan brûkers. Jo hawwe cookies útskeakele. Skeakelje dy opsje oan en besykje it nochris.", "nocookiesforlogin": "{{int:nocookieslogin}}", - "noname": "Jo moatte in meidognamme opjaan.", + "noname": "Jo hawwe gjin jildige meidochnamme opjûn.", "loginsuccesstitle": "Oanmelden slagge.", "loginsuccess": "Jo binne no oanmeld op {{SITENAME}} as \"$1\".", "nosuchuser": "Der is gjin meidogger \"$1\".\nKontrolearje de stavering, of [[Special:CreateAccount|meitsje in nije meidogger oan]].", @@ -888,7 +888,7 @@ "action-edit": "dizze side te bewurkjen", "action-createpage": "siden oan te meitsjen", "action-createtalk": "oerlissiden oan te meitsjen", - "action-createaccount": "dizze meidogger oan te meitsjen", + "action-createaccount": "oanmeitsjen fan dit meidochakkount", "action-minoredit": "dizze bewurking as lyts te markearjen", "action-move": "dizze side in oare namme te jaan", "action-move-subpages": "dizze side en de derby hearrende subsiden in oare namme te jaan", @@ -943,7 +943,7 @@ "rcshowhidebots": "bots $1", "rcshowhidebots-show": "werjaan", "rcshowhidebots-hide": "ferbergje", - "rcshowhideliu": "registrearre brûkers $1", + "rcshowhideliu": "Registrearre meidoggers $1", "rcshowhideliu-show": "werjaan", "rcshowhideliu-hide": "ferbergje", "rcshowhideanons": "$1 anonimen", @@ -1198,7 +1198,7 @@ "protectedpages-unknown-timestamp": "Unbekend", "protectedtitles": "Skoattele titels", "protectedtitlesempty": "Der binne op it stuit gjin sidenammen befeilige, dy't oan dizze betingsten foldogge.", - "listusers": "Meidoggerlist", + "listusers": "Meidoggerslist", "listusers-editsonly": "Allinne brûkers mei bewurkings werjaan", "listusers-creationsort": "Oarderje op dei fan oanmeitsjen", "usereditcount": "$1 {{PLURAL:$1|bewurking|bewurkings}}", @@ -1225,11 +1225,11 @@ "booksources-search": "Sykje", "booksources-text": "Hjirûnder is in list mei keppelings nei oare websites dy't nije of brûkte boeken ferkeapje en dy't faaks mear ynformaasje hawwe oer it boek dat jo sykje:", "booksources-invalid-isbn": "It ynjûne ISBN liket net jildich te wêzen.\nKontrolearje oft jo faaks in flater makke hawwe by de ynfier.", - "specialloguserlabel": "Útfierende meidogger:", - "speciallogtitlelabel": "Doel (titel of brûker):", + "specialloguserlabel": "Utfierder:", + "speciallogtitlelabel": "Doel (sidetitel of {{ns:user}}:meidochnamme foar meidogger):", "log": "Lochs", "all-logs-page": "Alle iepenbiere lochboeken", - "alllogstext": "Dit is it kombinearre logboek fan {{SITENAME}}.\nJo kinne ek kieze foar spesifike logboeken en filterje op brûker (haadstêfgefoelich) en sidenamme (haadstêfgefoelich).", + "alllogstext": "Gearfoege werjefte fan alle beskikbere lochs op {{SITENAME}}.\nJo kinne it byld beheine troch it kiezen fan in lochtype, de meidochnamme (haadlettergefoelich) of de oanbelangjende side (ek haadlettergefoelich).", "logempty": "Gjin treffers yn it loch.", "log-title-wildcard": "Siden sykje dy't mei dizze namme begjinne", "allpages": "Alle siden", @@ -1361,7 +1361,7 @@ "protect-locked-dblock": "It befeiligingsnivo kin net feroare wurde om't de database sletten is.\nHjir binne de hjoeddeiske ynstellingen foar de side '''$1''':", "protect-locked-access": "'''Jo brûker hat gjin rjochten om it befeiligingsnivo te feroarjen.'''\nDit binne de rinnende ynstellingen foar de side '''$1''':", "protect-cascadeon": "Dizze side is op 't stuit befeilige, om't er yn 'e folgjende {{PLURAL:$1|side|siden}} opnommen is, dy't befeilige {{PLURAL:$1|is|binne}} mei de kaskade-opsje. It befeiligingsnivo feroarje hat alhiel gjin effekt.", - "protect-default": "Tastean foar alle brûkers", + "protect-default": "Tastean foar alle meidoggers", "protect-fallback": "Hjir is it rjocht \"$1\" foar nedich", "protect-level-autoconfirmed": "Slút anonymen út", "protect-level-sysop": "Allinnich behearders", @@ -1407,7 +1407,7 @@ "namespace": "Nammeromte:", "invert": "Seleksje útsein", "blanknamespace": "(Haad)", - "contributions": "{{GENDER:$1|Meidogger}}-bydragen", + "contributions": "Bydragen fan 'e {{GENDER:$1|meidogger|meidochster}}", "contributions-title": "Bydragen fan $1", "mycontris": "Bydragen", "anoncontribs": "Bydragen", @@ -1416,7 +1416,7 @@ "uctop": "lêste feroaring", "month": "Fan moanne (en earder):", "year": "Fan jier (en earder):", - "sp-contributions-newbies": "Allinne bydragen fan nije brûkers besjen", + "sp-contributions-newbies": "Allinne bydragen fan nije akkounts besjen", "sp-contributions-newbies-sub": "Foar nijlingen", "sp-contributions-newbies-title": "Bydragen fan nije meidoggers", "sp-contributions-blocklog": "Blokkearlochboek", @@ -1424,7 +1424,7 @@ "sp-contributions-talk": "oerlis", "sp-contributions-userrights": "behear fan meidoggerrjochten", "sp-contributions-search": "Sykje nei bydragen", - "sp-contributions-username": "IP Adres of meidoggernamme:", + "sp-contributions-username": "IP-adres of meidochnamme:", "sp-contributions-submit": "Sykje", "whatlinkshere": "Wat is hjirmei keppele?", "whatlinkshere-title": "Siden dy't keppele binne mei \"$1\"", @@ -1556,7 +1556,7 @@ "importlogpage": "Ymportlochboek", "import-logentry-upload-detail": "$1 {{PLURAL:$1|ferzje|ferzjes}}", "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|ferzje|ferzjes}} fan $2", - "tooltip-pt-userpage": "Myn brûkersside", + "tooltip-pt-userpage": "Jo {{GENDER:|meidogger}}side", "tooltip-pt-mytalk": "Jo oerlisside", "tooltip-pt-preferences": "Myn foarkarynstellings", "tooltip-pt-watchlist": "List fan siden dy'sto besjochst op feroarings", @@ -1590,15 +1590,15 @@ "tooltip-t-recentchangeslinked": "De lêste feroarings yn siden dêr't dizze side nei ferwiisd", "tooltip-feed-rss": "RSS-feed foar dizze side", "tooltip-feed-atom": "Atom-feed foar dizze side", - "tooltip-t-contributions": "Bydragen fan dizze brûker", - "tooltip-t-emailuser": "Stjoer in e-mail nei {{GENDER:$1|dizze meidogger}}", + "tooltip-t-contributions": "List fan bydragen troch dizze {{GENDER:$1|meidogger|meidochster}}", + "tooltip-t-emailuser": "Stjoer in e-mail nei dizze {{GENDER:$1|meidogger|meidochster}}", "tooltip-t-upload": "Bestannen oplade", - "tooltip-t-specialpages": "List fan alle spesjale siden", + "tooltip-t-specialpages": "List fan alle bysûndere siden", "tooltip-t-print": "Ofdrukferzje fan dizze side", "tooltip-t-permalink": "Bliuwende keppeling nei dizze ferzje fan 'e side", "tooltip-ca-nstab-main": "Ynhâldlike side sjen litte", - "tooltip-ca-nstab-user": "Brûkersside sjen litte", - "tooltip-ca-nstab-special": "Dit is in spesjale side, dy't net bewurke wurde kin", + "tooltip-ca-nstab-user": "Besjoch de meidoggerside", + "tooltip-ca-nstab-special": "Dit is in bysûndere side, en kin net bewurke wurde", "tooltip-ca-nstab-project": "Projektside sjen litte", "tooltip-ca-nstab-image": "De bestânsside sjen litte", "tooltip-ca-nstab-mediawiki": "Systeemberjocht sjen litte", diff --git a/languages/i18n/he.json b/languages/i18n/he.json index ff3029f8a2..2dc84d9c01 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -3858,7 +3858,8 @@ "passwordpolicies-policy-maximalpasswordlength": "הסיסמה חייבת להיות קצרה יותר {{PLURAL:$1|מתו אחד|מ־$1 תווים}}", "passwordpolicies-policy-passwordcannotbepopular": "הסיסמה לא יכולה להיות זהה {{PLURAL:$1|לסיסמה נפוצה|לאחת הסיסמאות שנמצאות ברשימה של $1 הסיסמאות הנפוצות}}", "passwordpolicies-policy-passwordnotinlargeblacklist": "הסיסמה לא יכולה להיות ברשימת 100,000 הסיסמאות הנפוצות ביותר.", - "passwordpolicies-policyflag-forcechange": "נדרש שינוי הסיסמה בכניסה", + "passwordpolicies-policyflag-forcechange": "לדרוש שינוי בעת כניסה לחשבון", + "passwordpolicies-policyflag-suggestchangeonlogin": "להציע שינוי בעת כניסה לחשבון", "easydeflate-invaliddeflate": "התוכן שהועבר אינו דחוס כנדרש", "unprotected-js": "מסיבות אבטחה, לא ניתן לטעון JavaScript מדפים שאינם מוגנים. ניתן ליצור סקריפטי JavaScript רק במרחב השם \"מדיה ויקי:\" או בדפי משנה של דף המשתמש." } diff --git a/maintenance/findHooks.php b/maintenance/findHooks.php index ed7a762e16..900752fe4f 100644 --- a/maintenance/findHooks.php +++ b/maintenance/findHooks.php @@ -320,6 +320,7 @@ class FindHooks extends Maintenance { $iterator = new DirectoryIterator( $dir ); } + /** @var SplFileInfo $info */ foreach ( $iterator as $info ) { // Ignore directories, work only on php files, if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] ) diff --git a/maintenance/generateJsonI18n.php b/maintenance/generateJsonI18n.php index a7224b43b8..e9f4ecada3 100644 --- a/maintenance/generateJsonI18n.php +++ b/maintenance/generateJsonI18n.php @@ -79,6 +79,7 @@ class GenerateJsonI18n extends Maintenance { $dir_iterator = new RecursiveDirectoryIterator( dirname( $phpfile ) ); $iterator = new RecursiveIteratorIterator( $dir_iterator, RecursiveIteratorIterator::LEAVES_ONLY ); + /** @var SplFileInfo $fileObject */ foreach ( $iterator as $path => $fileObject ) { if ( fnmatch( "*.i18n.php", $fileObject->getFilename() ) ) { $this->output( "Converting $path.\n" ); diff --git a/maintenance/hhvm/makeRepo.php b/maintenance/hhvm/makeRepo.php index cef0dadc04..a8a0c7130d 100644 --- a/maintenance/hhvm/makeRepo.php +++ b/maintenance/hhvm/makeRepo.php @@ -149,6 +149,7 @@ class HHVMMakeRepo extends Maintenance { ), RecursiveIteratorIterator::LEAVES_ONLY ); + /** @var SplFileInfo $fileInfo */ foreach ( $iter as $file => $fileInfo ) { if ( $fileInfo->isFile() ) { $files[] = $file; diff --git a/tests/phpunit/includes/changetags/ChangeTagsTest.php b/tests/phpunit/includes/changetags/ChangeTagsTest.php index 0e209d533d..1405680b6f 100644 --- a/tests/phpunit/includes/changetags/ChangeTagsTest.php +++ b/tests/phpunit/includes/changetags/ChangeTagsTest.php @@ -592,7 +592,13 @@ class ChangeTagsTest extends MediaWikiTestCase { 'ctd_user_defined' => 1 ], ]; - $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' ); + $res = $dbr->select( + 'change_tag_def', + [ 'ctd_name', 'ctd_user_defined' ], + '', + __METHOD__, + [ 'ORDER BY' => 'ctd_name' ] + ); $this->assertEquals( $expected, iterator_to_array( $res, false ) ); } } diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php index 65b82abf0f..9679c6cec2 100644 --- a/tests/phpunit/includes/db/DatabaseTestHelper.php +++ b/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -45,7 +45,7 @@ class DatabaseTestHelper extends Database { public function __construct( $testName, array $opts = [] ) { $this->testName = $testName; - $this->profiler = new ProfilerStub( [] ); + $this->profiler = null; $this->trxProfiler = new TransactionProfiler(); $this->cliMode = $opts['cliMode'] ?? true; $this->connLogger = new \Psr\Log\NullLogger(); @@ -108,7 +108,11 @@ class DatabaseTestHelper extends Database { // Handle some internal calls from the Database class $check = $fname; - if ( preg_match( '/^Wikimedia\\\\Rdbms\\\\Database::query \((.+)\)$/', $fname, $m ) ) { + if ( preg_match( + '/^Wikimedia\\\\Rdbms\\\\Database::(?:query|beginIfImplied) \((.+)\)$/', + $fname, + $m + ) ) { $check = $m[1]; } diff --git a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php index f0f55fb6ac..b68ffaf8d6 100644 --- a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php @@ -26,6 +26,7 @@ class BagOStuffTest extends MediaWikiTestCase { } $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) ); + $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' ); } /** @@ -68,10 +69,25 @@ class BagOStuffTest extends MediaWikiTestCase { * @covers BagOStuff::mergeViaCas */ public function testMerge() { - $calls = 0; $key = $this->cache->makeKey( self::TEST_KEY ); - $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls ) { + $locks = false; + $checkLockingCallback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$locks ) { + $locks = $cache->get( "$key:lock" ); + + return false; + }; + + $this->cache->merge( $key, $checkLockingCallback, 5 ); + $this->assertFalse( $this->cache->get( $key ) ); + + $calls = 0; + $casRace = false; // emulate a race + $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls, &$casRace ) { ++$calls; + if ( $casRace ) { + // Uses CAS instead? + $cache->set( $key, 'conflict', 5 ); + } return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged'; }; @@ -87,21 +103,43 @@ class BagOStuffTest extends MediaWikiTestCase { $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) ); $calls = 0; - $this->cache->lock( $key ); - $this->assertFalse( $this->cache->merge( $key, $callback, 1 ), 'Non-blocking merge' ); - $this->cache->unlock( $key ); - $this->assertEquals( 0, $calls ); + if ( $locks ) { + // merge were something else already was merging (e.g. had the lock) + $this->cache->lock( $key ); + $this->assertFalse( + $this->cache->merge( $key, $callback, 5, 1 ), + 'Non-blocking merge (locking)' + ); + $this->cache->unlock( $key ); + $this->assertEquals( 0, $calls ); + } else { + $casRace = true; + $this->assertFalse( + $this->cache->merge( $key, $callback, 5, 1 ), + 'Non-blocking merge (CAS)' + ); + $this->assertEquals( 1, $calls ); + } } /** * @covers BagOStuff::merge * @covers BagOStuff::mergeViaLock + * @dataProvider provideTestMerge_fork */ - public function testMerge_fork() { + public function testMerge_fork( $exists, $winsLocking, $resLocking, $resCAS ) { $key = $this->cache->makeKey( self::TEST_KEY ); - $callback = function ( BagOStuff $cache, $key, $oldVal ) { - return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged'; + $pCallback = function ( BagOStuff $cache, $key, $oldVal ) { + return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent'; + }; + $cCallback = function ( BagOStuff $cache, $key, $oldVal ) { + return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child'; }; + + if ( $exists ) { + $this->cache->set( $key, 'x', 5 ); + } + /* * Test concurrent merges by forking this process, if: * - not manually called with --use-bagostuff @@ -115,17 +153,21 @@ class BagOStuffTest extends MediaWikiTestCase { $fork &= !$this->cache instanceof MultiWriteBagOStuff; if ( $fork ) { $pid = null; + $locked = false; // Function to start merge(), run another merge() midway through, then finish - $outerFunc = function ( BagOStuff $cache, $key, $oldVal ) use ( $callback, &$pid ) { + $func = function ( BagOStuff $cache, $key, $cur ) + use ( $pCallback, $cCallback, &$pid, &$locked ) + { $pid = pcntl_fork(); if ( $pid == -1 ) { return false; } elseif ( $pid ) { + $locked = $cache->get( "$key:lock" ); // parent has lock? pcntl_wait( $status ); - return $callback( $cache, $key, $oldVal ); + return $pCallback( $cache, $key, $cur ); } else { - $this->cache->merge( $key, $callback, 0, 1 ); + $this->cache->merge( $key, $cCallback, 0, 1 ); // Bail out of the outer merge() in the child process since it does not // need to attempt to write anything. Success is checked by the parent. parent::tearDown(); // avoid phpunit notices @@ -134,22 +176,34 @@ class BagOStuffTest extends MediaWikiTestCase { }; // attempt a merge - this should fail - $merged = $this->cache->merge( $key, $outerFunc, 0, 1 ); + $merged = $this->cache->merge( $key, $func, 0, 1 ); if ( $pid == -1 ) { return; // can't fork, ignore this test... } - // merge has failed because child process was merging (and we only attempted once) - $this->assertFalse( $merged ); - - // make sure the child's merge is completed and verify - $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' ); + if ( $locked ) { + // merge succeed since child was locked out + $this->assertEquals( $winsLocking, $merged ); + $this->assertEquals( $this->cache->get( $key ), $resLocking ); + } else { + // merge has failed because child process was merging (and we only attempted once) + $this->assertEquals( !$winsLocking, $merged ); + $this->assertEquals( $this->cache->get( $key ), $resCAS ); + } } else { $this->markTestSkipped( 'No pcntl methods available' ); } } + function provideTestMerge_fork() { + return [ + // (already exists, parent wins if locking, result if locking, result if CAS) + [ false, true, 'init-parent', 'init-child' ], + [ true, true, 'x-merged-parent', 'x-merged-child' ] + ]; + } + /** * @covers BagOStuff::changeTTL */ @@ -266,6 +320,34 @@ class BagOStuffTest extends MediaWikiTestCase { $this->cache->delete( $key4 ); } + /** + * @covers BagOStuff::setMulti + * @covers BagOStuff::deleteMulti + */ + public function testSetDeleteMulti() { + $map = [ + $this->cache->makeKey( 'test-1' ) => 'Siberian', + $this->cache->makeKey( 'test-2' ) => [ 'Huskies' ], + $this->cache->makeKey( 'test-3' ) => [ 'are' => 'the' ], + $this->cache->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ], + $this->cache->makeKey( 'test-5' ) => 4, + $this->cache->makeKey( 'test-6' ) => 'ever' + ]; + + $this->cache->setMulti( $map, 5 ); + $this->assertEquals( + $map, + $this->cache->getMulti( array_keys( $map ) ) + ); + + $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) ); + + $this->assertEquals( + [], + $this->cache->getMulti( array_keys( $map ) ) + ); + } + /** * @covers BagOStuff::getScopedLock */ diff --git a/tests/phpunit/includes/media/DjVuTest.php b/tests/phpunit/includes/media/DjVuTest.php index dbc0d2fbd5..d9b5d824dd 100644 --- a/tests/phpunit/includes/media/DjVuTest.php +++ b/tests/phpunit/includes/media/DjVuTest.php @@ -25,7 +25,7 @@ class DjVuTest extends MediaWikiMediaTestCase { } public function testGetImageSize() { - $this->assertArrayEquals( + $this->assertSame( [ 2480, 3508, 'DjVu', 'width="2480" height="3508"' ], $this->handler->getImageSize( null, $this->filePath . '/LoremIpsum.djvu' ), 'Test file LoremIpsum.djvu should have a size of 2480 * 3508' @@ -51,8 +51,8 @@ class DjVuTest extends MediaWikiMediaTestCase { public function testGetPageDimensions() { $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' ); - $this->assertArrayEquals( - [ 2480, 3508 ], + $this->assertSame( + [ 'width' => 2480, 'height' => 3508 ], $this->handler->getPageDimensions( $file, 1 ), 'Page 1 of test file LoremIpsum.djvu should have a size of 2480 * 3508' ); diff --git a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php index 584b141c2c..2ce097b5be 100644 --- a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php +++ b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php @@ -613,8 +613,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'Learner1', 'Learner2', 'Learner3', 'Learner4', 'Experienced1', ], - $this->fetchUsers( [ 'learner', 'experienced' ], $now ), - 'Learner and more experienced' + $this->fetchUsers( [ 'learner', 'experienced' ], $now ) ); } diff --git a/tests/phpunit/maintenance/categoryChangesAsRdfTest.php b/tests/phpunit/maintenance/categoryChangesAsRdfTest.php index f5a47d5101..521705e16e 100644 --- a/tests/phpunit/maintenance/categoryChangesAsRdfTest.php +++ b/tests/phpunit/maintenance/categoryChangesAsRdfTest.php @@ -242,7 +242,7 @@ class CategoryChangesAsRdfTest extends MediaWikiLangTestCase { $this->assertFileContains( $testFileName, $sparql ); $processed = $processedProperty->getValue( $dumpScript ); - $expectedProcessed = $preProcessed; + $expectedProcessed = array_keys( $preProcessed ); foreach ( $result as $row ) { if ( isset( $row->_processed ) ) { $this->assertArrayHasKey( $row->_processed, $processed, @@ -250,7 +250,7 @@ class CategoryChangesAsRdfTest extends MediaWikiLangTestCase { $expectedProcessed[] = $row->_processed; } } - $this->assertArrayEquals( $expectedProcessed, array_keys( $processed ), + $this->assertSame( $expectedProcessed, array_keys( $processed ), 'Processed array has wrong items' ); } diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php index 776dee1b4f..f41ab3a11f 100644 --- a/tests/phpunit/structure/ResourcesTest.php +++ b/tests/phpunit/structure/ResourcesTest.php @@ -155,7 +155,7 @@ class ResourcesTest extends MediaWikiTestCase { $css = file_get_contents( $basepath . 'comments.css' ); $files = CSSMin::getLocalFileReferences( $css, $basepath ); $expected = [ $basepath . 'not-commented.gif' ]; - $this->assertArrayEquals( + $this->assertSame( $expected, $files, 'Url(...) expression in comment should be omitted.'