Merge "Unset all globals unneeded for unit tests, assert correct directory"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 9 Jul 2019 18:57:55 +0000 (18:57 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 9 Jul 2019 18:57:55 +0000 (18:57 +0000)
24 files changed:
includes/installer/DatabaseInstaller.php
includes/installer/MssqlInstaller.php
includes/installer/MysqlInstaller.php
includes/installer/OracleInstaller.php
includes/installer/PostgresInstaller.php
includes/installer/SqliteInstaller.php
includes/jobqueue/JobQueueFederated.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/parser/PPDStack.php
includes/parser/PPFrame_DOM.php
includes/parser/PPNode.php
includes/parser/PPNode_DOM.php
includes/parser/PPNode_Hash_Tree.php
includes/parser/Parser.php
includes/parser/Preprocessor_DOM.php
includes/parser/Preprocessor_Hash.php
tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php
tests/phpunit/structure/SpecialPageFatalTest.php

index 8590e37..f947979 100644 (file)
@@ -49,7 +49,7 @@ abstract class DatabaseInstaller {
        /**
         * @var string Set by subclasses
         */
-       protected static $notMinimumVerisonMessage;
+       protected static $notMinimumVersionMessage;
 
        /**
         * The database connection.
@@ -82,7 +82,7 @@ abstract class DatabaseInstaller {
        public static function meetsMinimumRequirement( $serverVersion ) {
                if ( version_compare( $serverVersion, static::$minimumVersion ) < 0 ) {
                        return Status::newFatal(
-                               static::$notMinimumVerisonMessage, static::$minimumVersion, $serverVersion
+                               static::$notMinimumVersionMessage, static::$minimumVersion, $serverVersion
                        );
                }
 
index 45d6539..ffa17ed 100644 (file)
@@ -52,7 +52,7 @@ class MssqlInstaller extends DatabaseInstaller {
        // SQL Server 2005 RTM
        // @todo Are SQL Express version numbers different?)
        public static $minimumVersion = '9.00.1399';
-       protected static $notMinimumVerisonMessage = 'config-mssql-old';
+       protected static $notMinimumVersionMessage = 'config-mssql-old';
 
        // These are schema-level privs
        // Note: the web user will be created will full permissions if possible, this permission
index 6837eb8..3013db7 100644 (file)
@@ -51,7 +51,7 @@ class MysqlInstaller extends DatabaseInstaller {
        public $supportedEngines = [ 'InnoDB', 'MyISAM' ];
 
        public static $minimumVersion = '5.5.8';
-       protected static $notMinimumVerisonMessage = 'config-mysql-old';
+       protected static $notMinimumVersionMessage = 'config-mysql-old';
 
        public $webUserPrivs = [
                'DELETE',
index 644eec1..1a4e0f7 100644 (file)
@@ -47,7 +47,7 @@ class OracleInstaller extends DatabaseInstaller {
        ];
 
        public static $minimumVersion = '9.0.1'; // 9iR1
-       protected static $notMinimumVerisonMessage = 'config-oracle-old';
+       protected static $notMinimumVersionMessage = 'config-oracle-old';
 
        protected $connError = null;
 
index 6592c51..d6a5145 100644 (file)
@@ -47,7 +47,7 @@ class PostgresInstaller extends DatabaseInstaller {
        ];
 
        public static $minimumVersion = '9.2';
-       protected static $notMinimumVerisonMessage = 'config-postgres-old';
+       protected static $notMinimumVersionMessage = 'config-postgres-old';
        public $maxRoleSearchDepth = 5;
 
        protected $pgConns = [];
index 37d9153..17332ff 100644 (file)
@@ -34,7 +34,7 @@ use Wikimedia\Rdbms\DBConnectionError;
 class SqliteInstaller extends DatabaseInstaller {
 
        public static $minimumVersion = '3.8.0';
-       protected static $notMinimumVerisonMessage = 'config-outdated-sqlite';
+       protected static $notMinimumVersionMessage = 'config-outdated-sqlite';
 
        /**
         * @var DatabaseSqlite
index 8b5a62e..beab4c6 100644 (file)
@@ -88,8 +88,6 @@ class JobQueueFederated extends JobQueue {
                ) {
                        unset( $baseConfig[$o] ); // partition queue doesn't care about this
                }
-               // The class handles all aggregator calls already
-               unset( $baseConfig['aggregator'] );
                // Get the partition queue objects
                foreach ( $partitionMap as $partition => $w ) {
                        if ( !isset( $params['configByPartition'][$partition] ) ) {
index f493769..42146f4 100644 (file)
@@ -878,6 +878,14 @@ EOT;
 
                $mime = 'application/zip';
                $opendocTypes = [
+                       # In OASIS Open Document Format v1.2, Database front end document
+                       # has a recommended MIME type of:
+                       # application/vnd.oasis.opendocument.base
+                       # Despite the type registered at the IANA being 'database' which is
+                       # supposed to be normative.
+                       # T35515
+                       'base',
+
                        'chart-template',
                        'chart',
                        'formula-template',
@@ -895,7 +903,10 @@ EOT;
                        'text-web',
                        'text' ];
 
-               // https://lists.oasis-open.org/archives/office/200505/msg00006.html
+               // The list of document types is available in OASIS Open Document
+               // Format version 1.2 under Appendix C. It is not normative though,
+               // supposedly types registered at the IANA should be.
+               // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html
                $types = '(?:' . implode( '|', $opendocTypes ) . ')';
                $opendocRegex = "/^mimetype(application\/vnd\.oasis\.opendocument\.$types)/";
 
index 00bf57d..7ebbe8b 100644 (file)
@@ -652,15 +652,19 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
 
        /**
         * Delete all objects expiring before a certain date.
-        * @param string $date The reference date in MW format
-        * @param callable|bool $progressCallback Optional, a function which will be called
+        * @param string|int $timestamp The reference date in MW or TS_UNIX format
+        * @param callable|null $progressCallback 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( $date, $progressCallback = false, $limit = INF ) {
+       public function deleteObjectsExpiringBefore(
+               $timestamp,
+               callable $progressCallback = null,
+               $limit = INF
+       ) {
                // stub
                return false;
        }
index 0bdd349..e193497 100644 (file)
@@ -82,9 +82,18 @@ class CachedBagOStuff extends HashBagOStuff {
                $this->backend->setDebug( $bool );
        }
 
-       public function deleteObjectsExpiringBefore( $date, $progressCallback = false, $limit = INF ) {
-               parent::deleteObjectsExpiringBefore( $date, $progressCallback, $limit );
-               return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback, $limit );
+       public function deleteObjectsExpiringBefore(
+               $timestamp,
+               callable $progressCallback = null,
+               $limit = INF
+       ) {
+               parent::deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit );
+
+               return $this->backend->deleteObjectsExpiringBefore(
+                       $timestamp,
+                       $progressCallback,
+                       $limit
+               );
        }
 
        public function makeKeyInternal( $keyspace, $args ) {
index 7ca04ee..4c6750f 100644 (file)
@@ -208,10 +208,14 @@ class MultiWriteBagOStuff extends BagOStuff {
                return $this->caches[0]->unlock( $key );
        }
 
-       public function deleteObjectsExpiringBefore( $date, $progressCallback = false, $limit = INF ) {
+       public function deleteObjectsExpiringBefore(
+               $timestamp,
+               callable $progressCallback = null,
+               $limit = INF
+       ) {
                $ret = false;
                foreach ( $this->caches as $cache ) {
-                       if ( $cache->deleteObjectsExpiringBefore( $date, $progressCallback, $limit ) ) {
+                       if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit ) ) {
                                $ret = true;
                        }
                }
index 6fac0ad..8502ce2 100644 (file)
@@ -108,8 +108,16 @@ class ReplicatedBagOStuff extends BagOStuff {
                return $this->writeStore->unlock( $key );
        }
 
-       public function deleteObjectsExpiringBefore( $date, $progressCallback = false, $limit = INF ) {
-               return $this->writeStore->deleteObjectsExpiringBefore( $date, $progressCallback );
+       public function deleteObjectsExpiringBefore(
+               $timestamp,
+               callable $progressCallback = null,
+               $limit = INF
+       ) {
+               return $this->writeStore->deleteObjectsExpiringBefore(
+                       $timestamp,
+                       $progressCallback,
+                       $limit
+               );
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
index 2487920..2c533b9 100644 (file)
@@ -163,6 +163,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        const COOLOFF_TTL = 1;
        /** Default remaining TTL at which to consider pre-emptive regeneration */
        const LOW_TTL = 30;
+       /** Max TTL 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;
@@ -173,20 +175,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        /** Seconds to ramp up to the "popularity" refresh chance after a key is no longer new */
        const RAMPUP_TTL = 30;
 
-       /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
+       /** Idiom for getWithSetCallback() meaning "do not store the callback result" */
        const TTL_UNCACHEABLE = -1;
-       /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
+       /** Idiom for getWithSetCallback() meaning "no regeneration mutex based on key hotness" */
        const TSE_NONE = -1;
-       /** Max TTL to store keys when a data sourced is lagged */
-       const TTL_LAGGED = 30;
-       /** Idiom for delete() for "no hold-off" */
-       const HOLDOFF_NONE = 0;
-       /** Idiom for set()/getWithSetCallback() for "do not augment the storage medium TTL" */
+       /** Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */
        const STALE_TTL_NONE = 0;
-       /** Idiom for set()/getWithSetCallback() for "no post-expired grace period" */
+       /** 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;
 
-       /** Idiom for getWithSetCallback() for "no minimum required as-of timestamp" */
+       /** Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */
        const MIN_TIMESTAMP_NONE = 0.0;
 
        /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
@@ -1213,72 +1213,65 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * @note Callable type hints are not used to avoid class-autoloading
         */
        final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
+               $version = $opts['version'] ?? null;
                $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
 
                // Try the process cache if enabled and the cache callback is not within a cache callback.
                // Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
                // the in-memory value is further lagged than the shared one since it uses a blind TTL.
                if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
-                       $group = $opts['pcGroup'] ?? self::PC_PRIMARY;
-                       $procCache = $this->getProcessCache( $group );
-                       $value = $procCache->has( $key, $pcTTL ) ? $procCache->get( $key ) : false;
+                       $procCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
+                       if ( $procCache->has( $key, $pcTTL ) ) {
+                               return $procCache->get( $key );
+                       }
                } else {
-                       $procCache = false;
-                       $value = false;
+                       $procCache = null;
                }
 
-               if ( $value === false ) {
-                       // Fetch the value over the network
-                       if ( isset( $opts['version'] ) ) {
-                               $version = $opts['version'];
-                               $asOf = null;
-                               $cur = $this->doGetWithSetCallback(
-                                       $key,
+               if ( $version !== null ) {
+                       $curAsOf = self::PASS_BY_REF;
+                       $curValue = $this->doGetWithSetCallback(
+                               $key,
+                               $ttl,
+                               // Wrap the value in an array with version metadata but hide it from $callback
+                               function ( $oldValue, &$ttl, &$setOpts, $oldAsOf ) use ( $callback, $version ) {
+                                       if ( $this->isVersionedValue( $oldValue, $version ) ) {
+                                               $oldData = $oldValue[self::VFLD_DATA];
+                                       } else {
+                                               // VFLD_DATA is not set if an old, unversioned, key is present
+                                               $oldData = false;
+                                               $oldAsOf = null;
+                                       }
+
+                                       return [
+                                               self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
+                                               self::VFLD_VERSION => $version
+                                       ];
+                               },
+                               $opts,
+                               $curAsOf
+                       );
+                       if ( $this->isVersionedValue( $curValue, $version ) ) {
+                               // Current value has the requested version; use it
+                               $value = $curValue[self::VFLD_DATA];
+                       } else {
+                               // Current value has a different version; use the variant key for this version.
+                               // Regenerate the variant value if it is not newer than the main value at $key
+                               // so that purges to they key propagate to the variant value.
+                               $value = $this->doGetWithSetCallback(
+                                       $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
                                        $ttl,
-                                       function ( $oldValue, &$ttl, &$setOpts, $oldAsOf )
-                                       use ( $callback, $version ) {
-                                               if ( is_array( $oldValue )
-                                                       && array_key_exists( self::VFLD_DATA, $oldValue )
-                                                       && array_key_exists( self::VFLD_VERSION, $oldValue )
-                                                       && $oldValue[self::VFLD_VERSION] === $version
-                                               ) {
-                                                       $oldData = $oldValue[self::VFLD_DATA];
-                                               } else {
-                                                       // VFLD_DATA is not set if an old, unversioned, key is present
-                                                       $oldData = false;
-                                                       $oldAsOf = null;
-                                               }
-
-                                               return [
-                                                       self::VFLD_DATA => $callback( $oldData, $ttl, $setOpts, $oldAsOf ),
-                                                       self::VFLD_VERSION => $version
-                                               ];
-                                       },
-                                       $opts,
-                                       $asOf
+                                       $callback,
+                                       [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts
                                );
-                               if ( $cur[self::VFLD_VERSION] === $version ) {
-                                       // Value created or existed before with version; use it
-                                       $value = $cur[self::VFLD_DATA];
-                               } else {
-                                       // Value existed before with a different version; use variant key.
-                                       // Reflect purges to $key by requiring that this key value be newer.
-                                       $value = $this->doGetWithSetCallback(
-                                               $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
-                                               $ttl,
-                                               $callback,
-                                               // Regenerate value if not newer than $key
-                                               [ 'version' => null, 'minAsOf' => $asOf ] + $opts
-                                       );
-                               }
-                       } else {
-                               $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
                        }
+               } else {
+                       $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
+               }
 
-                       // Update the process cache if enabled
-                       if ( $procCache && $value !== false ) {
-                               $procCache->set( $key, $value );
-                       }
+               // Update the process cache if enabled
+               if ( $procCache && $value !== false ) {
+                       $procCache->set( $key, $value );
                }
 
                return $value;
@@ -1306,26 +1299,25 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $busyValue = $opts['busyValue'] ?? null;
                $popWindow = $opts['hotTTR'] ?? self::HOT_TTR;
                $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
-               $minTime = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
-               $needsVersion = isset( $opts['version'] );
+               $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
                $touchedCb = $opts['touchedCallback'] ?? null;
                $initialTime = $this->getCurrentTime();
 
                $kClass = $this->determineKeyClassForStats( $key );
 
-               // Get the current key value
+               // Get the current key value and metadata
                $curTTL = self::PASS_BY_REF;
                $curInfo = self::PASS_BY_REF; /** @var array $curInfo */
                $curValue = $this->get( $key, $curTTL, $checkKeys, $curInfo );
                // Apply any $touchedCb invalidation timestamp to get the "last purge timestamp"
                list( $curTTL, $LPT ) = $this->resolveCTL( $curValue, $curTTL, $curInfo, $touchedCb );
-               // Keep track of the best candidate value and its timestamp
-               $value = $curValue; // return value
-               $asOf = $curInfo['asOf']; // return value timestamp
+               // Best possible return value and its corresponding "as of" timestamp
+               $value = $curValue;
+               $asOf = $curInfo['asOf'];
 
                // Determine if a cached value regeneration is needed or desired
                if (
-                       $this->isValid( $value, $needsVersion, $asOf, $minTime ) &&
+                       $this->isValid( $value, $asOf, $minAsOf ) &&
                        $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
                ) {
                        $preemptiveRefresh = (
@@ -1347,7 +1339,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $isKeyTombstoned = ( $curInfo['tombAsOf'] !== null );
                if ( $isKeyTombstoned ) {
                        // Get the interim key value since the key is tombstoned (write-holed)
-                       list( $value, $asOf ) = $this->getInterimValue( $key, $needsVersion, $minTime );
+                       list( $value, $asOf ) = $this->getInterimValue( $key, $minAsOf );
                        // Update the "last purge time" since the $touchedCb timestamp depends on $value
                        $LPT = $this->resolveTouched( $value, $LPT, $touchedCb );
                }
@@ -1355,7 +1347,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Reduce mutex and cache set spam while keys are in the tombstone/holdoff period by
                // checking if $value was genereated by a recent thread much less than a second ago.
                if (
-                       $this->isValid( $value, $needsVersion, $asOf, $minTime, $LPT ) &&
+                       $this->isValid( $value, $asOf, $minAsOf, $LPT ) &&
                        $this->isVolatileValueAgeNegligible( $initialTime - $asOf )
                ) {
                        $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
@@ -1381,12 +1373,12 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                $hasLock = false;
                if ( $useMutex ) {
-                       // Acquire a datacenter-local non-blocking lock
+                       // Attempt to acquire a non-blocking lock specific to the local datacenter
                        if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
                                // Lock acquired; this thread will recompute the value and update cache
                                $hasLock = true;
-                       } elseif ( $this->isValid( $value, $needsVersion, $asOf, $minTime ) ) {
-                               // Lock not acquired and a stale value exists; use the stale value
+                       } elseif ( $this->isValid( $value, $asOf, $minAsOf ) ) {
+                               // Not acquired and stale cache value exists; use the stale value
                                $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
 
                                return $value;
@@ -1394,7 +1386,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                                // Lock not acquired and no stale value exists
                                if ( $busyValue !== null ) {
                                        // Use the busy fallback value if nothing else
-                                       $miss = is_infinite( $minTime ) ? 'renew' : 'miss';
+                                       $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
                                        $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
 
                                        return is_callable( $busyValue ) ? $busyValue() : $busyValue;
@@ -1419,7 +1411,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
                if ( $valueIsCacheable ) {
                        $ago = max( $this->getCurrentTime() - $initialTime, 0.0 );
-                       $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1000 * $ago );
+                       $this->stats->timing( "wanobjectcache.$kClass.regen_set_delay", 1e3 * $ago );
 
                        if ( $isKeyTombstoned ) {
                                if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
@@ -1445,7 +1437,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                        $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$initialTime - 60 );
                }
 
-               $miss = is_infinite( $minTime ) ? 'renew' : 'miss';
+               $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
                $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
 
                return $value;
@@ -1540,11 +1532,10 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
 
        /**
         * @param string $key
-        * @param bool $versioned
-        * @param float $minTime
+        * @param float $minAsOf Minimum acceptable "as of" timestamp
         * @return array (cached value or false, cached value timestamp or null)
         */
-       protected function getInterimValue( $key, $versioned, $minTime ) {
+       protected function getInterimValue( $key, $minAsOf ) {
                if ( !$this->useInterimHoldOffCaching ) {
                        return [ false, null ]; // disabled
                }
@@ -1552,7 +1543,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
                list( $value ) = $this->unwrap( $wrapped, $this->getCurrentTime() );
                $valueAsOf = $wrapped[self::FLD_TIME] ?? null;
-               if ( $this->isValid( $value, $versioned, $valueAsOf, $minTime ) ) {
+               if ( $this->isValid( $value, $valueAsOf, $minAsOf ) ) {
                        return [ $value, $valueAsOf ];
                }
 
@@ -2116,9 +2107,8 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                // Update the cache value later, such during post-send of an HTTP request
                $func = $this->asyncHandler;
                $func( function () use ( $key, $ttl, $callback, $opts ) {
-                       $asOf = null; // unused
                        $opts['minAsOf'] = INF; // force a refresh
-                       $this->doGetWithSetCallback( $key, $ttl, $callback, $opts, $asOf );
+                       $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
                } );
 
                return true;
@@ -2226,21 +2216,18 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         * Check if $value is not false, versioned (if needed), and not older than $minTime (if set)
         *
         * @param array|bool $value
-        * @param bool $versioned
         * @param float $asOf The time $value was generated
-        * @param float $minTime The last time the main value was generated (0.0 if unknown)
+        * @param float $minAsOf Minimum acceptable "as of" timestamp
         * @param float|null $purgeTime The last time the value was invalidated
         * @return bool
         */
-       protected function isValid( $value, $versioned, $asOf, $minTime, $purgeTime = null ) {
+       protected function isValid( $value, $asOf, $minAsOf, $purgeTime = null ) {
                // Avoid reading any key not generated after the latest delete() or touch
-               $safeMinTime = max( $minTime, $purgeTime + self::TINY_POSTIVE );
+               $safeMinAsOf = max( $minAsOf, $purgeTime + self::TINY_POSTIVE );
 
                if ( $value === false ) {
                        return false;
-               } elseif ( $versioned && !isset( $value[self::VFLD_VERSION] ) ) {
-                       return false;
-               } elseif ( $safeMinTime > 0 && $asOf < $minTime ) {
+               } elseif ( $safeMinAsOf > 0 && $asOf < $minAsOf ) {
                        return false;
                }
 
@@ -2372,6 +2359,20 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
        }
 
+       /**
+        * @param mixed $value
+        * @param int $version
+        * @return bool
+        */
+       protected function isVersionedValue( $value, $version ) {
+               return (
+                       is_array( $value ) &&
+                       array_key_exists( self::VFLD_DATA, $value ) &&
+                       array_key_exists( self::VFLD_VERSION, $value ) &&
+                       $value[self::VFLD_VERSION] === $version
+               );
+       }
+
        /**
         * @param string $group
         * @return MapCacheLRU
index 2ef94c4..c3a5897 100644 (file)
@@ -43,8 +43,8 @@ class SqlBagOStuff extends BagOStuff {
        protected $serverTags;
        /** @var int */
        protected $numServers;
-       /** @var int */
-       protected $lastExpireAll = 0;
+       /** @var int UNIX timestamp */
+       protected $lastGarbageCollect = 0;
        /** @var int */
        protected $purgePeriod = 10;
        /** @var int */
@@ -67,6 +67,9 @@ class SqlBagOStuff extends BagOStuff {
        /** @var array Exceptions */
        protected $connFailureErrors = [];
 
+       /** @var int */
+       const GARBAGE_COLLECT_DELAY_SEC = 1;
+
        /**
         * Constructor. Parameters are:
         *   - server:      A server info structure in the format required by each
@@ -338,8 +341,6 @@ class SqlBagOStuff extends BagOStuff {
                        $keysByTable[$serverIndex][$tableName][] = $key;
                }
 
-               $this->garbageCollect(); // expire old entries if any
-
                $result = true;
                $exptime = (int)$expiry;
                /** @noinspection PhpUnusedLocalVariableInspection */
@@ -348,6 +349,7 @@ class SqlBagOStuff extends BagOStuff {
                        $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
+                               $this->occasionallyGarbageCollect( $db );
                        } catch ( DBError $e ) {
                                $this->handleWriteError( $e, $db, $serverIndex );
                                $result = false;
@@ -601,7 +603,10 @@ class SqlBagOStuff extends BagOStuff {
         * @return bool
         */
        protected function isExpired( $db, $exptime ) {
-               return $exptime != $this->getMaxDateTime( $db ) && wfTimestamp( TS_UNIX, $exptime ) < time();
+               return (
+                       $exptime != $this->getMaxDateTime( $db ) &&
+                       wfTimestamp( TS_UNIX, $exptime ) < time()
+               );
        }
 
        /**
@@ -616,116 +621,147 @@ class SqlBagOStuff extends BagOStuff {
                }
        }
 
-       protected function garbageCollect() {
-               if ( !$this->purgePeriod || $this->replicaOnly ) {
-                       // Disabled
-                       return;
-               }
-               // Only purge on one in every $this->purgePeriod writes
-               if ( $this->purgePeriod !== 1 && mt_rand( 0, $this->purgePeriod - 1 ) ) {
-                       return;
-               }
-               $now = time();
-               // Avoid repeating the delete within a few seconds
-               if ( $now > ( $this->lastExpireAll + 1 ) ) {
-                       $this->lastExpireAll = $now;
-                       $this->deleteObjectsExpiringBefore(
-                               wfTimestamp( TS_MW, $now ),
-                               false,
-                               $this->purgeLimit
-                       );
+       /**
+        * @param IDatabase $db
+        * @throws DBError
+        */
+       protected function occasionallyGarbageCollect( IDatabase $db ) {
+               if (
+                       // Random purging is enabled
+                       $this->purgePeriod &&
+                       // This is not using a replica DB
+                       !$this->replicaOnly &&
+                       // Only purge on one in every $this->purgePeriod writes
+                       mt_rand( 0, $this->purgePeriod - 1 ) == 0 &&
+                       // Avoid repeating the delete within a few seconds
+                       ( time() - $this->lastGarbageCollect ) > self::GARBAGE_COLLECT_DELAY_SEC
+               ) {
+                       $garbageCollector = function () use ( $db ) {
+                               $this->deleteServerObjectsExpiringBefore( $db, time(), null, $this->purgeLimit );
+                               $this->lastGarbageCollect = time();
+                       };
+                       if ( $this->asyncHandler ) {
+                               $this->lastGarbageCollect = time(); // avoid duplicate enqueues
+                               ( $this->asyncHandler )( $garbageCollector );
+                       } else {
+                               $garbageCollector();
+                       }
                }
        }
 
        public function expireAll() {
-               $this->deleteObjectsExpiringBefore( wfTimestampNow() );
+               $this->deleteObjectsExpiringBefore( time() );
        }
 
        public function deleteObjectsExpiringBefore(
                $timestamp,
-               $progressCallback = false,
+               callable $progressCallback = null,
                $limit = INF
        ) {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
 
-               $count = 0;
-               for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+               $serverIndexes = range( 0, $this->numServers - 1 );
+               shuffle( $serverIndexes );
+
+               $ok = true;
+
+               $keysDeletedCount = 0;
+               foreach ( $serverIndexes as $numServersDone => $serverIndex ) {
                        $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
-                               $dbTimestamp = $db->timestamp( $timestamp );
-                               $totalSeconds = false;
-                               $baseConds = [ 'exptime < ' . $db->addQuotes( $dbTimestamp ) ];
-                               for ( $i = 0; $i < $this->shards; $i++ ) {
-                                       $maxExpTime = false;
-                                       while ( true ) {
-                                               $conds = $baseConds;
-                                               if ( $maxExpTime !== false ) {
-                                                       $conds[] = 'exptime >= ' . $db->addQuotes( $maxExpTime );
-                                               }
-                                               $rows = $db->select(
-                                                       $this->getTableNameByShard( $i ),
-                                                       [ 'keyname', 'exptime' ],
-                                                       $conds,
-                                                       __METHOD__,
-                                                       [ 'LIMIT' => 100, 'ORDER BY' => 'exptime' ]
-                                               );
-                                               if ( $rows === false || !$rows->numRows() ) {
-                                                       break;
-                                               }
-                                               $keys = [];
-                                               $row = $rows->current();
-                                               $minExpTime = $row->exptime;
-                                               if ( $totalSeconds === false ) {
-                                                       $totalSeconds = wfTimestamp( TS_UNIX, $timestamp )
-                                                               - wfTimestamp( TS_UNIX, $minExpTime );
-                                               }
-                                               foreach ( $rows as $row ) {
-                                                       $keys[] = $row->keyname;
-                                                       $maxExpTime = $row->exptime;
-                                               }
-
-                                               $db->delete(
-                                                       $this->getTableNameByShard( $i ),
-                                                       [
-                                                               'exptime >= ' . $db->addQuotes( $minExpTime ),
-                                                               'exptime < ' . $db->addQuotes( $dbTimestamp ),
-                                                               'keyname' => $keys
-                                                       ],
-                                                       __METHOD__
-                                               );
-                                               $count += $db->affectedRows();
-                                               if ( $count >= $limit ) {
-                                                       return true;
-                                               }
-
-                                               if ( is_callable( $progressCallback ) ) {
-                                                       if ( intval( $totalSeconds ) === 0 ) {
-                                                               $percent = 0;
-                                                       } else {
-                                                               $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp )
-                                                                       - wfTimestamp( TS_UNIX, $maxExpTime );
-                                                               if ( $remainingSeconds > $totalSeconds ) {
-                                                                       $totalSeconds = $remainingSeconds;
-                                                               }
-                                                               $processedSeconds = $totalSeconds - $remainingSeconds;
-                                                               $percent = ( $i + $processedSeconds / $totalSeconds )
-                                                                       / $this->shards * 100;
-                                                       }
-                                                       $percent = ( $percent / $this->numServers )
-                                                               + ( $serverIndex / $this->numServers * 100 );
-                                                       call_user_func( $progressCallback, $percent );
-                                               }
-                                       }
-                               }
+                               $this->deleteServerObjectsExpiringBefore(
+                                       $db,
+                                       $timestamp,
+                                       $progressCallback,
+                                       $limit,
+                                       $numServersDone,
+                                       $keysDeletedCount
+                               );
                        } catch ( DBError $e ) {
                                $this->handleWriteError( $e, $db, $serverIndex );
-                               return false;
+                               $ok = false;
                        }
                }
 
-               return true;
+               return $ok;
+       }
+
+       /**
+        * @param IDatabase $db
+        * @param string|int $timestamp
+        * @param callable|null $progressCallback
+        * @param int $limit
+        * @param int $serversDoneCount
+        * @param int &$keysDeletedCount
+        * @throws DBError
+        */
+       private function deleteServerObjectsExpiringBefore(
+               IDatabase $db,
+               $timestamp,
+               $progressCallback,
+               $limit,
+               $serversDoneCount = 0,
+               &$keysDeletedCount = 0
+       ) {
+               $cutoffUnix = wfTimestamp( TS_UNIX, $timestamp );
+               $shardIndexes = range( 0, $this->shards - 1 );
+               shuffle( $shardIndexes );
+
+               foreach ( $shardIndexes as $numShardsDone => $shardIndex ) {
+                       $continue = null; // last exptime
+                       $lag = null; // purge lag
+                       do {
+                               $res = $db->select(
+                                       $this->getTableNameByShard( $shardIndex ),
+                                       [ 'keyname', 'exptime' ],
+                                       array_merge(
+                                               [ 'exptime < ' . $db->addQuotes( $db->timestamp( $cutoffUnix ) ) ],
+                                               $continue ? [ 'exptime >= ' . $db->addQuotes( $continue ) ] : []
+                                       ),
+                                       __METHOD__,
+                                       [ 'LIMIT' => min( $limit, 100 ), 'ORDER BY' => 'exptime' ]
+                               );
+
+                               if ( $res->numRows() ) {
+                                       $row = $res->current();
+                                       if ( $lag === null ) {
+                                               $lag = max( $cutoffUnix - wfTimestamp( TS_UNIX, $row->exptime ), 1 );
+                                       }
+
+                                       $keys = [];
+                                       foreach ( $res as $row ) {
+                                               $keys[] = $row->keyname;
+                                               $continue = $row->exptime;
+                                       }
+
+                                       $db->delete(
+                                               $this->getTableNameByShard( $shardIndex ),
+                                               [
+                                                       'exptime < ' . $db->addQuotes( $db->timestamp( $cutoffUnix ) ),
+                                                       'keyname' => $keys
+                                               ],
+                                               __METHOD__
+                                       );
+                                       $keysDeletedCount += $db->affectedRows();
+                               }
+
+                               if ( is_callable( $progressCallback ) ) {
+                                       if ( $lag ) {
+                                               $remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue );
+                                               $processedLag = max( $lag - $remainingLag, 0 );
+                                               $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->shards;
+                                       } else {
+                                               $doneRatio = 1;
+                                       }
+
+                                       $overallRatio = ( $doneRatio / $this->numServers )
+                                               + ( $serversDoneCount / $this->numServers );
+                                       call_user_func( $progressCallback, $overallRatio * 100 );
+                               }
+                       } while ( $res->numRows() && $keysDeletedCount < $limit );
+               }
        }
 
        /**
@@ -763,6 +799,8 @@ class SqlBagOStuff extends BagOStuff {
                }
 
                list( $serverIndex ) = $this->getTableByKey( $key );
+
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $ok = $db->lock( $key, __METHOD__, $timeout );
@@ -793,6 +831,8 @@ class SqlBagOStuff extends BagOStuff {
                        unset( $this->locks[$key] );
 
                        list( $serverIndex ) = $this->getTableByKey( $key );
+
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                                $ok = $db->unlock( $key, __METHOD__ );
index 4108bd7..adc0bc0 100644 (file)
@@ -27,7 +27,7 @@ class PPDStack {
        public $stack, $rootAccum;
 
        /**
-        * @var PPDStack
+        * @var PPDStack|false
         */
        public $top;
        public $out;
index 03ee6d9..452bab1 100644 (file)
@@ -141,7 +141,7 @@ class PPFrame_DOM implements PPFrame {
        /**
         * @throws MWException
         * @param string|int $key
-        * @param string|PPNode_DOM|DOMDocument $root
+        * @param string|PPNode_DOM|DOMNode|DOMNodeList $root
         * @param int $flags
         * @return string
         */
@@ -152,7 +152,7 @@ class PPFrame_DOM implements PPFrame {
 
        /**
         * @throws MWException
-        * @param string|PPNode_DOM|DOMDocument $root
+        * @param string|PPNode_DOM|DOMNode $root
         * @param int $flags
         * @return string
         */
@@ -396,7 +396,7 @@ class PPFrame_DOM implements PPFrame {
        /**
         * @param string $sep
         * @param int $flags
-        * @param string|PPNode_DOM|DOMDocument ...$args
+        * @param string|PPNode_DOM|DOMNode ...$args
         * @return string
         */
        public function implodeWithFlags( $sep, $flags, ...$args ) {
@@ -426,7 +426,7 @@ class PPFrame_DOM implements PPFrame {
         * This previously called implodeWithFlags but has now been inlined to reduce stack depth
         *
         * @param string $sep
-        * @param string|PPNode_DOM|DOMDocument ...$args
+        * @param string|PPNode_DOM|DOMNode ...$args
         * @return string
         */
        public function implode( $sep, ...$args ) {
@@ -456,7 +456,7 @@ class PPFrame_DOM implements PPFrame {
         * with implode()
         *
         * @param string $sep
-        * @param string|PPNode_DOM|DOMDocument ...$args
+        * @param string|PPNode_DOM|DOMNode ...$args
         * @return array
         */
        public function virtualImplode( $sep, ...$args ) {
@@ -487,7 +487,7 @@ class PPFrame_DOM implements PPFrame {
         * @param string $start
         * @param string $sep
         * @param string $end
-        * @param string|PPNode_DOM|DOMDocument ...$args
+        * @param string|PPNode_DOM|DOMNode ...$args
         * @return array
         */
        public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
index 2b6cf7c..4561657 100644 (file)
@@ -36,20 +36,20 @@ interface PPNode {
        /**
         * Get an array-type node containing the children of this node.
         * Returns false if this is not a tree node.
-        * @return PPNode
+        * @return false|PPNode
         */
        public function getChildren();
 
        /**
         * Get the first child of a tree node. False if there isn't one.
         *
-        * @return PPNode
+        * @return false|PPNode
         */
        public function getFirstChild();
 
        /**
         * Get the next sibling of any node. False if there isn't one
-        * @return PPNode
+        * @return false|PPNode
         */
        public function getNextSibling();
 
@@ -57,7 +57,7 @@ interface PPNode {
         * Get all children of this tree node which have a given name.
         * Returns an array-type node, or false if this is not a tree node.
         * @param string $type
-        * @return bool|PPNode
+        * @return false|PPNode
         */
        public function getChildrenOfType( $type );
 
index 26a4791..53b1761 100644 (file)
@@ -27,7 +27,7 @@
 class PPNode_DOM implements PPNode {
 
        /**
-        * @var DOMElement
+        * @var DOMElement|DOMNodeList
         */
        public $node;
        public $xpath;
@@ -59,21 +59,21 @@ class PPNode_DOM implements PPNode {
        }
 
        /**
-        * @return bool|PPNode_DOM
+        * @return false|PPNode_DOM
         */
        public function getChildren() {
                return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
        }
 
        /**
-        * @return bool|PPNode_DOM
+        * @return false|PPNode_DOM
         */
        public function getFirstChild() {
                return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
        }
 
        /**
-        * @return bool|PPNode_DOM
+        * @return false|PPNode_DOM
         */
        public function getNextSibling() {
                return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
@@ -82,7 +82,7 @@ class PPNode_DOM implements PPNode {
        /**
         * @param string $type
         *
-        * @return bool|PPNode_DOM
+        * @return false|PPNode_DOM
         */
        public function getChildrenOfType( $type ) {
                return new self( $this->getXPath()->query( $type, $this->node ) );
index e6cabf8..7782894 100644 (file)
@@ -135,7 +135,7 @@ class PPNode_Hash_Tree implements PPNode {
         * return a temporary proxy object: different instances will be returned
         * if this is called more than once on the same node.
         *
-        * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|bool
+        * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|false
         */
        public function getFirstChild() {
                if ( !isset( $this->rawChildren[0] ) ) {
@@ -150,7 +150,7 @@ class PPNode_Hash_Tree implements PPNode {
         * return a temporary proxy object: different instances will be returned
         * if this is called more than once on the same node.
         *
-        * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|bool
+        * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|false
         */
        public function getNextSibling() {
                return self::factory( $this->store, $this->index + 1 );
index 4808caf..a2c5eec 100644 (file)
@@ -205,9 +205,11 @@ class Parser {
        public $mLinkID;
        public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
        public $mDefaultSort;
-       public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
+       public $mTplRedirCache, $mHeadings, $mDoubleUnderscores;
        public $mExpensiveFunctionCount; # number of expensive parser function calls
        public $mShowToc, $mForceTocPosition;
+       /** @var array */
+       public $mTplDomCache;
 
        /**
         * @var User
@@ -3084,7 +3086,7 @@ class Parser {
         *  self::OT_HTML: all templates and extension tags
         *
         * @param string $text The text to transform
-        * @param bool|PPFrame $frame Object describing the arguments passed to the
+        * @param false|PPFrame|array $frame Object describing the arguments passed to the
         *   template. Arguments may also be provided as an associative array, as
         *   was the usual case before MW1.12. Providing arguments this way may be
         *   useful for extensions wishing to perform variable replacement
@@ -3190,7 +3192,7 @@ class Parser {
         *   $piece['lineStart']: whether the brace was at the start of a line
         * @param PPFrame $frame The current frame, contains template arguments
         * @throws Exception
-        * @return string The text of the template
+        * @return string|array The text of the template
         */
        public function braceSubstitution( $piece, $frame ) {
                // Flags
index 9e510d2..fb8a1dc 100644 (file)
@@ -37,6 +37,9 @@ class Preprocessor_DOM extends Preprocessor {
 
        const CACHE_PREFIX = 'preprocess-xml';
 
+       /**
+        * @param Parser $parser
+        */
        public function __construct( $parser ) {
                wfDeprecated( __METHOD__, '1.34' ); // T204945
                $this->parser = $parser;
index 66f081f..f7f37ac 100644 (file)
@@ -50,6 +50,9 @@ class Preprocessor_Hash extends Preprocessor {
        const CACHE_PREFIX = 'preprocess-hash';
        const CACHE_VERSION = 2;
 
+       /**
+        * @param Parser $parser
+        */
        public function __construct( $parser ) {
                $this->parser = $parser;
        }
index 1947812..0f23b8c 100644 (file)
@@ -137,4 +137,30 @@ class MimeAnalyzerTest extends PHPUnit\Framework\TestCase {
                $actualType = $this->doGuessMimeType( [ $file, 'doc' ] );
                $this->assertEquals( 'application/msword', $actualType );
        }
+
+       /**
+        * @covers MimeAnalyzer::detectZipType
+        * @dataProvider provideOpendocumentsformatHeaders
+        */
+       function testDetectZipTypeRecognizesOpendocuments( $expected, $header ) {
+               $this->assertEquals(
+                       $expected,
+                       $this->mimeAnalyzer->detectZipType( $header )
+               );
+       }
+
+       /**
+        * An ODF file is a ZIP file of multiple files. The first one being
+        * 'mimetype' and is not compressed.
+        */
+       function provideOpendocumentsformatHeaders() {
+               $thirtychars = str_repeat( 0, 30 );
+               return [
+                       'Database front end document header based on ODF 1.2' => [
+                               'application/vnd.oasis.opendocument.base',
+                               $thirtychars . 'mimetypeapplication/vnd.oasis.opendocument.basePK',
+                       ],
+               ];
+       }
+
 }
index 3fa31fe..1366119 100644 (file)
@@ -30,7 +30,7 @@ class SpecialPageFatalTest extends MediaWikiTestCase {
                $user = User::newFromName( 'UTSysop' );
 
                try {
-                       $executor->executeSpecialPage( $page, '', null, null, $user );
+                       $executor->executeSpecialPage( $page, '', null, 'qqx', $user );
                } catch ( \PHPUnit\Framework\Error\Deprecated $deprecated ) {
                        // Allow deprecation,
                        // this test want to check fatals or other things breaking the extension