From 6fa1e56daa33273f3fba97a70be79f7d151c8470 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Fri, 2 Sep 2016 15:44:59 -0700 Subject: [PATCH] resourceloader: Shorten cache expiry if 'version' query doesn't match Versioned load.php requests (load.php?modules=..&version=..) are highly cacheable due to being versioned. When a module changes, the startup module delivers new metadata to the client which naturally results in a cache miss by producing a different url. However, during upgrades and other deployments it was possible for a multi-server installation to pollute edge caches with outdated content in the following scenario: * Deployment starts for an upgrade from version A to version B. * Server 1 (on version B) gets a request for the startup manifest. Client receives new manifest with version B information and makes a versioned request for version B. * Server 2 (still on version A) responds to this request with the version it has (which is version A). * Edge cache will store version A under the new url for version B. This commit changes the last point by setting a low max-age when a request has a 'version' hash mismatch. Test plan: * Look for a request in your browser like: '/load.php?..modules=jquery%2Cmediawiki..&version=..' * Verify it has a high Cache-Control max-age. * Manipulate the 'version' parameter and verify it gets a low max-age. Bug: T117587 Change-Id: Iba89c09b7b71d9fd2a8ff3ebe2618e26ea9daddf --- includes/resourceloader/ResourceLoader.php | 54 ++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 9ce2c5a6f3..d9cd0ee35b 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -610,17 +610,49 @@ class ResourceLoader implements LoggerAwareInterface { * * @since 1.26 * @param ResourceLoaderContext $context - * @param array $modules List of ResourceLoaderModule objects + * @param string[] $modules List of known module names * @return string Hash */ - public function getCombinedVersion( ResourceLoaderContext $context, array $modules ) { - if ( !$modules ) { + public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) { + if ( !$moduleNames ) { return ''; } $hashes = array_map( function ( $module ) use ( $context ) { return $this->getModule( $module )->getVersionHash( $context ); - }, $modules ); - return self::makeHash( implode( $hashes ) ); + }, $moduleNames ); + return self::makeHash( implode( '', $hashes ) ); + } + + /** + * Get the expected value of the 'version' query parameter. + * + * This is used by respond() to set a short Cache-Control header for requests with + * information newer than the current server has. This avoids pollution of edge caches. + * Typically during deployment. (T117587) + * + * This MUST match return value of `mw.loader#getCombinedVersion()` client-side. + * + * @since 1.28 + * @param ResourceLoaderContext $context + * @param string[] $modules List of module names + * @return string Hash + */ + public function getExpectedVersionQuery( ResourceLoaderContext $context ) { + // As of MediaWiki 1.28, the server and client use the same algorithm for combining + // version hashes. There is no technical reason for this to be same, and for years the + // implementations differed. If getCombinedVersion in PHP (used for StartupModule and + // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version' + // query parameter), then this method must continue to match the JS one. + $moduleNames = []; + foreach ( $context->getModules() as $name ) { + if ( !$this->getModule( $name ) ) { + // If a versioned request contains a missing module, the version is a mismatch + // as the client considered a module (and version) we don't have. + return ''; + } + $moduleNames[] = $name; + } + return $this->getCombinedVersion( $context, $moduleNames ); } /** @@ -759,10 +791,14 @@ class ResourceLoader implements LoggerAwareInterface { */ protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) { $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); - // If a version wasn't specified we need a shorter expiry time for updates - // to propagate to clients quickly - // If there were errors, we also need a shorter expiry time so we can recover quickly - if ( is_null( $context->getVersion() ) || $errors ) { + // Use a short cache expiry so that updates propagate to clients quickly, if: + // - No version specified (shared resources, e.g. stylesheets) + // - There were errors (recover quickly) + // - Version mismatch (T117587, T47877) + if ( is_null( $context->getVersion() ) + || $errors + || $context->getVersion() !== $this->getExpectedVersionQuery( $context ) + ) { $maxage = $rlMaxage['unversioned']['client']; $smaxage = $rlMaxage['unversioned']['server']; // If a version was specified we can use a longer expiry time since changing -- 2.20.1