Merge "Set $wgBlockAllowsUTEdit = true; by default"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 24 Sep 2015 01:14:35 +0000 (01:14 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 24 Sep 2015 01:14:35 +0000 (01:14 +0000)
autoload.php
includes/FileContentsHasher.php [new file with mode: 0644]
includes/libs/objectcache/WANObjectCache.php
includes/media/GIF.php
includes/media/PNG.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderModule.php
tests/phpunit/includes/media/GIFTest.php
tests/phpunit/includes/media/PNGTest.php

index 0c03243..e5e7cda 100644 (file)
@@ -437,6 +437,7 @@ $wgAutoloadLocalClasses = array(
        'FileBackendStoreShardListIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
        'FileBasedSiteLookup' => __DIR__ . '/includes/site/FileBasedSiteLookup.php',
        'FileCacheBase' => __DIR__ . '/includes/cache/FileCacheBase.php',
+       'FileContentsHasher' => __DIR__ . '/includes/FileContentsHasher.php',
        'FileDeleteForm' => __DIR__ . '/includes/FileDeleteForm.php',
        'FileDependency' => __DIR__ . '/includes/cache/CacheDependency.php',
        'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php',
diff --git a/includes/FileContentsHasher.php b/includes/FileContentsHasher.php
new file mode 100644 (file)
index 0000000..67eb9d2
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Generate hash digests of file contents to help with cache invalidation.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+class FileContentsHasher {
+
+       /** @var BagOStuff */
+       protected $cache;
+
+       /** @var FileContentsHasher */
+       private static $instance;
+
+       /**
+        * Constructor.
+        */
+       public function __construct() {
+               $this->cache = ObjectCache::newAccelerator( 'hash' );
+       }
+
+       /**
+        * Get the singleton instance of this class.
+        *
+        * @return FileContentsHasher
+        */
+       public static function singleton() {
+               if ( !self::$instance ) {
+                       self::$instance = new self;
+               }
+
+               return self::$instance;
+       }
+
+       /**
+        * Get a hash of a file's contents, either by retrieving a previously-
+        * computed hash from the cache, or by computing a hash from the file.
+        *
+        * @private
+        * @param string $filePath Full path to the file.
+        * @param string $algo Name of selected hashing algorithm.
+        * @return string|bool Hash of file contents, or false if the file could not be read.
+        */
+       public function getFileContentsHashInternal( $filePath, $algo = 'md4' ) {
+               $mtime = MediaWiki\quietCall( 'filemtime', $filePath );
+               if ( $mtime === false ) {
+                       return false;
+               }
+
+               $cacheKey = wfGlobalCacheKey( __CLASS__, $filePath, $mtime, $algo );
+               $hash = $this->cache->get( $cacheKey );
+
+               if ( $hash ) {
+                       return $hash;
+               }
+
+               $contents = MediaWiki\quietCall( 'file_get_contents', $filePath );
+               if ( $contents === false ) {
+                       return false;
+               }
+
+               $hash = hash( $algo, $contents );
+               $this->cache->set( $cacheKey, $hash, 60 * 60 * 24 );  // 24h
+
+               return $hash;
+       }
+
+       /**
+        * Get a hash of the combined contents of one or more files, either by
+        * retrieving a previously-computed hash from the cache, or by computing
+        * a hash from the files.
+        *
+        * @param string|string[] $filePaths One or more file paths.
+        * @param string $algo Name of selected hashing algorithm.
+        * @return string|bool Hash of files' contents, or false if no file could not be read.
+        */
+       public static function getFileContentsHash( $filePaths, $algo = 'md4' ) {
+               $instance = self::singleton();
+
+               if ( !is_array( $filePaths ) ) {
+                       $filePaths = (array) $filePaths;
+               }
+
+               if ( count( $filePaths ) === 1 ) {
+                       return $instance->getFileContentsHashInternal( $filePaths[0], $algo );
+               }
+
+               sort( $filePaths );
+               $hashes = array_map( function ( $filePath ) use ( $instance, $algo ) {
+                       return $instance->getFileContentsHashInternal( $filePath, $algo ) ?: '';
+               }, $filePaths );
+
+               $hashes = implode( '', $hashes );
+               return $hashes ? hash( $algo, $hashes ) : false;
+       }
+}
index a382e1f..41dc66f 100644 (file)
@@ -75,13 +75,15 @@ class WANObjectCache {
        const LOCK_TTL = 5;
        /** Default remaining TTL at which to consider pre-emptive regeneration */
        const LOW_TTL = 10;
-       /** Default TTL for temporarily caching tombstoned keys */
-       const TEMP_TTL = 5;
+       /** Default time-since-expiry on a miss that makes a key "hot" */
+       const LOCK_TSE = 1;
 
        /** Idiom for set()/getWithSetCallback() TTL */
        const TTL_NONE = 0;
        /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
        const TTL_UNCACHEABLE = -1;
+       /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
+       const TSE_NONE = -1;
 
        /** Cache format version number */
        const VERSION = 1;
@@ -472,6 +474,7 @@ class WANObjectCache {
         *   - lowTTL  : consider pre-emptive updates when the current TTL (sec)
         *               of the key is less than this. It becomes more likely
         *               over time, becoming a certainty once the key is expired.
+        *               [Default: WANObjectCache::LOW_TTL seconds]
         *   - lockTSE : if the key is tombstoned or expired (by $checkKeys) less
         *               than this many seconds ago, then try to have a single
         *               thread handle cache regeneration at any given time.
@@ -479,17 +482,16 @@ class WANObjectCache {
         *               If, on miss, the time since expiration is low, the assumption
         *               is that the key is hot and that a stampede is worth avoiding.
         *               Setting this above WANObjectCache::HOLDOFF_TTL makes no difference.
-        *   - tempTTL : TTL of the temp key used to cache values while a key is tombstoned.
-        *               This avoids excessive regeneration of hot keys on delete() but may
-        *               result in stale values.
+        *               The higher this is set, the higher the worst-case staleness can be.
+        *               Use WANObjectCache::TSE_NONE to disable this logic.
+        *               [Default: WANObjectCache::TSE_NONE]
         * @return mixed Value to use for the key
         */
        final public function getWithSetCallback(
                $key, $callback, $ttl, array $checkKeys = array(), array $opts = array()
        ) {
                $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
-               $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : -1;
-               $tempTTL = isset( $opts['tempTTL'] ) ? $opts['tempTTL'] : self::TEMP_TTL;
+               $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
 
                // Get the current key value
                $curTTL = null;
@@ -505,9 +507,14 @@ class WANObjectCache {
                $isTombstone = ( $curTTL !== null && $value === false );
                // Assume a key is hot if requested soon after invalidation
                $isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
+               // Decide whether a single thread should handle regenerations.
+               // This avoids stampedes when $checkKeys are bumped and when preemptive
+               // renegerations take too long. It also reduces regenerations while $key
+               // is tombstoned. This balances cache freshness with avoiding DB load.
+               $useMutex = ( $isHot || ( $isTombstone && $lockTSE > 0 ) );
 
                $lockAcquired = false;
-               if ( $isHot ) {
+               if ( $useMutex ) {
                        // Acquire a cluster-local non-blocking lock
                        if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
                                // Lock acquired; this thread should update the key
@@ -515,16 +522,14 @@ class WANObjectCache {
                        } elseif ( $value !== false ) {
                                // If it cannot be acquired; then the stale value can be used
                                return $value;
-                       }
-               }
-
-               if ( !$lockAcquired && ( $isTombstone || $isHot ) ) {
-                       // Use the stash value for tombstoned keys to reduce regeneration load.
-                       // For hot keys, either another thread has the lock or the lock failed;
-                       // use the stash value from the last thread that regenerated it.
-                       $value = $this->cache->get( self::STASH_KEY_PREFIX . $key );
-                       if ( $value !== false ) {
-                               return $value;
+                       } else {
+                               // Use the stash value for tombstoned keys to reduce regeneration load.
+                               // For hot keys, either another thread has the lock or the lock failed;
+                               // use the stash value from the last thread that regenerated it.
+                               $value = $this->cache->get( self::STASH_KEY_PREFIX . $key );
+                               if ( $value !== false ) {
+                                       return $value;
+                               }
                        }
                }
 
@@ -536,7 +541,8 @@ class WANObjectCache {
                $value = call_user_func_array( $callback, array( $cValue, &$ttl ) );
                // When delete() is called, writes are write-holed by the tombstone,
                // so use a special stash key to pass the new value around threads.
-               if ( $value !== false && ( $isHot || $isTombstone ) && $ttl >= 0 ) {
+               if ( $useMutex && $value !== false && $ttl >= 0 ) {
+                       $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
                        $this->cache->set( self::STASH_KEY_PREFIX . $key, $value, $tempTTL );
                }
 
index 94aca61..b998d18 100644 (file)
@@ -187,4 +187,25 @@ class GIFHandler extends BitmapHandler {
 
                return $wgLang->commaList( $info );
        }
+
+       /**
+        * Return the duration of the GIF file.
+        *
+        * Shown in the &query=imageinfo&iiprop=size api query.
+        *
+        * @param $file File
+        * @return float The duration of the file.
+        */
+       public function getLength( $file ) {
+               $serMeta = $file->getMetadata();
+               MediaWiki\suppressWarnings();
+               $metadata = unserialize( $serMeta );
+               MediaWiki\restoreWarnings();
+
+               if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
+                       return 0.0;
+               } else {
+                       return (float)$metadata['duration'];
+               }
+       }
 }
index c3f0832..e297fe1 100644 (file)
@@ -175,9 +175,29 @@ class PNGHandler extends BitmapHandler {
                return $wgLang->commaList( $info );
        }
 
+       /**
+        * Return the duration of an APNG file.
+        *
+        * Shown in the &query=imageinfo&iiprop=size api query.
+        *
+        * @param $file File
+        * @return float The duration of the file.
+        */
+       public function getLength( $file ) {
+               $serMeta = $file->getMetadata();
+               MediaWiki\suppressWarnings();
+               $metadata = unserialize( $serMeta );
+               MediaWiki\restoreWarnings();
+
+               if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
+                       return 0.0;
+               } else {
+                       return (float)$metadata['duration'];
+               }
+       }
+
        // PNGs should be easy to support, but it will need some sharpening applied
        // and another user test to check if the perceived quality change is noticeable
-
        public function supportsBucketing() {
                return false;
        }
index 7fbc1cb..51118b1 100644 (file)
@@ -966,12 +966,44 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * @return string CSS source
         */
        protected function compileLessFile( $fileName, $compiler = null ) {
+               static $cache;
+
+               if ( !$cache ) {
+                       $cache = ObjectCache::newAccelerator( CACHE_ANYTHING );
+               }
+
+               // Construct a cache key from the LESS file name and a hash digest
+               // of the LESS variables used for compilation.
+               $varsHash = hash( 'md4', serialize( ResourceLoader::getLessVars( $this->getConfig() ) ) );
+               $cacheKey = wfGlobalCacheKey( 'LESS', $fileName, $varsHash );
+               $cachedCompile = $cache->get( $cacheKey );
+
+               // If we got a cached value, we have to validate it by getting a
+               // checksum of all the files that were loaded by the parser and
+               // ensuring it matches the cached entry's.
+               if ( isset( $cachedCompile['hash'] ) ) {
+                       $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
+                       if ( $contentHash === $cachedCompile['hash'] ) {
+                               $this->localFileRefs += $cachedCompile['files'];
+                               return $cachedCompile['css'];
+                       }
+               }
+
                if ( !$compiler ) {
                        $compiler = $this->getLessCompiler();
                }
-               $result = $compiler->parseFile( $fileName )->getCss();
-               $this->localFileRefs += array_keys( $compiler->AllParsedFiles() );
-               return $result;
+
+               $css = $compiler->parseFile( $fileName )->getCss();
+               $files = $compiler->AllParsedFiles();
+               $this->localFileRefs = array_merge( $this->localFileRefs, $files );
+
+               $cache->set( $cacheKey, array(
+                       'css'   => $css,
+                       'files' => $files,
+                       'hash'  => FileContentsHasher::getFileContentsHash( $files ),
+               ), 60 * 60 * 24 );  // 86400 seconds, or 24 hours.
+
+               return $css;
        }
 
        /**
index 1d3ffb5..376b62c 100644 (file)
@@ -857,35 +857,6 @@ abstract class ResourceLoaderModule {
         * @return string Hash
         */
        protected static function safeFileHash( $filePath ) {
-               static $cache;
-
-               if ( !$cache ) {
-                       $cache = ObjectCache::newAccelerator( CACHE_NONE );
-               }
-
-               MediaWiki\suppressWarnings();
-               $mtime = filemtime( $filePath );
-               MediaWiki\restoreWarnings();
-               if ( !$mtime ) {
-                       return '';
-               }
-
-               $cacheKey = wfGlobalCacheKey( 'resourceloader', __METHOD__, $filePath );
-               $cachedHash = $cache->get( $cacheKey );
-               if ( isset( $cachedHash['mtime'] ) && $cachedHash['mtime'] === $mtime ) {
-                       return $cachedHash['hash'];
-               }
-
-               MediaWiki\suppressWarnings();
-               $contents = file_get_contents( $filePath );
-               MediaWiki\restoreWarnings();
-               if ( !$contents ) {
-                       return '';
-               }
-
-               $hash = hash( 'md4', $contents );
-               $cache->set( $cacheKey, array( 'mtime' => $mtime, 'hash' => $hash ), 60 * 60 * 24 );
-
-               return $hash;
+               return FileContentsHasher::getFileContentsHash( $filePath );
        }
 }
index 87ffd99..de1e153 100644 (file)
@@ -139,4 +139,24 @@ class GIFHandlerTest extends MediaWikiMediaTestCase {
                        ),
                );
        }
+
+       /**
+        * @param $filename string
+        * @param $expectedLength float
+        * @dataProvider provideGetLength
+        */
+       public function testGetLength( $filename, $expectedLength ) {
+               $file = $this->dataFile( $filename, 'image/gif' );
+               $actualLength = $file->getLength();
+               $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 );
+       }
+
+       public function provideGetLength() {
+               return array(
+                       array( 'animated.gif', 2.4 ),
+                       array( 'animated-xmp.gif', 2.4 ),
+                       array( 'nonanimated', 0.0 ),
+                       array( 'Bishzilla_blink.gif', 1.4 ),
+               );
+       }
 }
index 36872a7..96ede90 100644 (file)
@@ -128,4 +128,24 @@ class PNGHandlerTest extends MediaWikiMediaTestCase {
                        ),
                );
        }
+
+       /**
+        * @param $filename string
+        * @param $expectedLength float
+        * @dataProvider provideGetLength
+        */
+       public function testGetLength( $filename, $expectedLength ) {
+               $file = $this->dataFile( $filename, 'image/png' );
+               $actualLength = $file->getLength();
+               $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 );
+       }
+
+       public function provideGetLength() {
+               return array(
+                       array( 'Animated_PNG_example_bouncing_beach_ball.png', 1.5 ),
+                       array( 'Png-native-test.png', 0.0 ),
+                       array( 'greyscale-png.png', 0.0 ),
+                       array( '1bit-png.png', 0.0 ),
+               );
+       }
 }