Add ability to pre-render thumbnails at upload time
authorGilles Dubuc <gdubuc@wikimedia.org>
Fri, 29 Aug 2014 14:50:18 +0000 (16:50 +0200)
committerGilles Dubuc <gdubuc@wikimedia.org>
Fri, 12 Sep 2014 07:20:41 +0000 (09:20 +0200)
This introduces an option that allows one to define a list of
thumbnail sizes to be rendered by async jobs at upload time.

Change-Id: Ida193699738c79aca333fa78b8b536d14a410841
Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/301

includes/AutoLoader.php
includes/DefaultSettings.php
includes/jobqueue/jobs/ThumbnailRenderJob.php [new file with mode: 0644]
includes/upload/UploadBase.php

index 04802f9..8e08335 100644 (file)
@@ -647,6 +647,7 @@ $wgAutoloadLocalClasses = array(
        'UploadFromUrlJob' => 'includes/jobqueue/jobs/UploadFromUrlJob.php',
        'AssembleUploadChunksJob' => 'includes/jobqueue/jobs/AssembleUploadChunksJob.php',
        'PublishStashedFileJob' => 'includes/jobqueue/jobs/PublishStashedFileJob.php',
+       'ThumbnailRenderJob' => 'includes/jobqueue/jobs/ThumbnailRenderJob.php',
 
        # includes/jobqueue/utils
        'BacklinkJobUtils' => 'includes/jobqueue/utils/BacklinkJobUtils.php',
index 5fc7377..245c87b 100644 (file)
@@ -1242,6 +1242,46 @@ $wgThumbnailBuckets = null;
  */
 $wgThumbnailMinimumBucketDistance = 50;
 
+/**
+ * When defined, is an array of thumbnail widths to be rendered at upload time. The idea is to
+ * prerender common thumbnail sizes, in order to avoid the necessity to render them on demand, which
+ * has a performance impact for the first client to view a certain size.
+ *
+ * This obviously means that more disk space is needed per upload upfront.
+ *
+ * @since 1.24
+ */
+
+$wgUploadThumbnailRenderMap = array();
+
+/**
+ * The method through which the thumbnails will be prerendered for the entries in
+ * $wgUploadThumbnailRenderMap
+ *
+ * The method can be either "http" or "jobqueue". The former uses an http request to hit the
+ * thumbnail's URL.
+ * This method only works if thumbnails are configured to be rendered by a 404 handler. The latter
+ * option uses the job queue to render the thumbnail.
+ *
+ * @since 1.24
+ */
+$wgUploadThumbnailRenderMethod = 'jobqueue';
+
+/**
+ * When using the "http" wgUploadThumbnailRenderMethod, lets one specify a custom Host HTTP header.
+ *
+ * @since 1.24
+ */
+$wgUploadThumbnailRenderHttpCustomHost = false;
+
+/**
+ * When using the "http" wgUploadThumbnailRenderMethod, lets one specify a custom domain to send the
+ * HTTP request to.
+ *
+ * @since 1.24
+ */
+$wgUploadThumbnailRenderHttpCustomDomain = false;
+
 /**
  * Default parameters for the "<gallery>" tag
  */
@@ -6291,6 +6331,7 @@ $wgJobClasses = array(
        'uploadFromUrl' => 'UploadFromUrlJob',
        'AssembleUploadChunks' => 'AssembleUploadChunksJob',
        'PublishStashedFile' => 'PublishStashedFileJob',
+       'ThumbnailRender' => 'ThumbnailRenderJob',
        'null' => 'NullJob'
 );
 
diff --git a/includes/jobqueue/jobs/ThumbnailRenderJob.php b/includes/jobqueue/jobs/ThumbnailRenderJob.php
new file mode 100644 (file)
index 0000000..ec68d7e
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Job for asynchronous rendering of thumbnails.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup JobQueue
+ */
+
+/**
+ * Job for asynchronous rendering of thumbnails.
+ *
+ * @ingroup JobQueue
+ */
+class ThumbnailRenderJob extends Job {
+       public function __construct( $title, $params ) {
+               parent::__construct( 'ThumbnailRender', $title, $params );
+       }
+
+       public function run() {
+               global $wgUploadThumbnailRenderMethod;
+
+               $transformParams = $this->params['transformParams'];
+
+               $file = wfLocalFile( $this->title );
+
+               if ( $file && $file->exists() ) {
+                       if ( $wgUploadThumbnailRenderMethod === 'jobqueue' ) {
+                               $thumb = $file->transform( $transformParams, File::RENDER_NOW );
+
+                               if ( $thumb && !$thumb->isError() ) {
+                                       return true;
+                               } else {
+                                       $this->setLastError( __METHOD__ . ': thumbnail couln\'t be generated' );
+                                       return false;
+                               }
+                       } elseif ( $wgUploadThumbnailRenderMethod === 'http' ) {
+                               $status = $this->hitThumbUrl( $file, $transformParams );
+
+                               wfDebug( __METHOD__ . ": received status {$status}\n" );
+
+                               if ( $status === 200 || $status === 301 || $status === 302 ) {
+                                       return true;
+                               } elseif ( $status ) {
+                                       // Note that this currently happens (500) when requesting sizes larger then or
+                                       // equal to the original, which is harmless.
+                                       $this->setLastError( __METHOD__ . ': incorrect HTTP status ' . $status );
+                                       return false;
+                               } else {
+                                       $this->setLastError( __METHOD__ . ': HTTP request failure' );
+                                       return false;
+                               }
+                       } else {
+                               $this->setLastError( __METHOD__ . ': unknown thumbnail render method ' . $wgUploadThumbnailRenderMethod );
+                               return false;
+                       }
+               } else {
+                       $this->setLastError( __METHOD__ . ': file doesn\'t exist' );
+                       return false;
+               }
+       }
+
+       protected function hitThumbUrl( $file, $transformParams ) {
+               global $wgUploadThumbnailRenderHttpCustomHost, $wgUploadThumbnailRenderHttpCustomDomain;
+
+               $thumbName = $file->thumbName( $transformParams );
+               $thumbUrl = $file->getThumbUrl( $thumbName );
+
+               if ( $wgUploadThumbnailRenderHttpCustomDomain ) {
+                       $thumbUrl = '//' . $wgUploadThumbnailRenderHttpCustomDomain . parse_url( $thumbUrl, PHP_URL_PATH );
+               }
+
+               wfDebug( __METHOD__ . ": hitting url {$thumbUrl}\n" );
+
+               $request = MWHttpRequest::factory( $thumbUrl, array(
+                       'method' => 'HEAD',
+                       'followRedirects' => true ) );
+
+               if ( $wgUploadThumbnailRenderHttpCustomHost ) {
+                       $request->setHeader( 'Host', $wgUploadThumbnailRenderHttpCustomHost );
+               }
+
+               $status = $request->execute();
+
+               return $request->getStatus();
+       }
+}
index 5de543e..369fcd1 100644 (file)
@@ -746,6 +746,8 @@ abstract class UploadBase {
                                );
                        }
                        wfRunHooks( 'UploadComplete', array( &$this ) );
+
+                       $this->postProcessUpload();
                }
 
                wfProfileOut( __METHOD__ );
@@ -753,6 +755,26 @@ abstract class UploadBase {
                return $status;
        }
 
+       /**
+        * Perform extra steps after a successful upload.
+        */
+       public function postProcessUpload() {
+               global $wgUploadThumbnailRenderMap;
+
+               $jobs = array();
+
+               $sizes = $wgUploadThumbnailRenderMap;
+               rsort( $sizes );
+
+               foreach ( $sizes as $size ) {
+                       $jobs []= new ThumbnailRenderJob( $this->getLocalFile()->getTitle(), array(
+                               'transformParams' => array( 'width' => $size ),
+                       ) );
+               }
+
+               JobQueueGroup::singleton()->push( $jobs );
+       }
+
        /**
         * Returns the title of the file to be uploaded. Sets mTitleError in case
         * the name was illegal.