From cf6a935e1c523131dfe6084881e403284fa68406 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Tue, 10 Apr 2012 14:26:11 +0200 Subject: [PATCH] CacheHelper: facilitate caching handling on a page CacheHelper changes: adding a CacheHelper object to facilitate caching several things on a single page and created implementations for SpecialPage and Action also containing scaffolding for things such as purging the cache Change-Id: I7b1f8ac8ac2e37f15b3924d4448e579e6dbbc80f --- includes/AutoLoader.php | 4 + includes/CacheHelper.php | 363 ++++++++++++++++++++++++ includes/actions/CachedAction.php | 170 +++++++++++ includes/specials/SpecialCachedPage.php | 169 +++++++++++ languages/messages/MessagesEn.php | 5 + languages/messages/MessagesQqq.php | 4 +- maintenance/language/messages.inc | 6 + 7 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 includes/CacheHelper.php create mode 100644 includes/actions/CachedAction.php create mode 100644 includes/specials/SpecialCachedPage.php diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 56f3af6b5b..497ab901e1 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -27,6 +27,7 @@ $wgAutoloadLocalClasses = array( 'BadTitleError' => 'includes/Exception.php', 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', + 'CacheHelper' => 'includes/CacheHelper.php', 'Category' => 'includes/Category.php', 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', @@ -121,6 +122,7 @@ $wgAutoloadLocalClasses = array( 'Http' => 'includes/HttpFunctions.php', 'HttpError' => 'includes/Exception.php', 'HttpRequest' => 'includes/HttpFunctions.old.php', + 'ICacheHelper' => 'includes/CacheHelper.php', 'IcuCollation' => 'includes/Collation.php', 'IdentityCollation' => 'includes/Collation.php', 'ImageGallery' => 'includes/ImageGallery.php', @@ -260,6 +262,7 @@ $wgAutoloadLocalClasses = array( 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', # includes/actions + 'CachedAction' => 'includes/actions/CachedAction.php', 'CreditsAction' => 'includes/actions/CreditsAction.php', 'DeleteAction' => 'includes/actions/DeleteAction.php', 'EditAction' => 'includes/actions/EditAction.php', @@ -827,6 +830,7 @@ $wgAutoloadLocalClasses = array( 'SpecialBlockList' => 'includes/specials/SpecialBlockList.php', 'SpecialBlockme' => 'includes/specials/SpecialBlockme.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', + 'SpecialCachedPage' => 'includes/specials/SpecialCachedPage.php', 'SpecialCategories' => 'includes/specials/SpecialCategories.php', 'SpecialChangeEmail' => 'includes/specials/SpecialChangeEmail.php', 'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php', diff --git a/includes/CacheHelper.php b/includes/CacheHelper.php new file mode 100644 index 0000000000..6b6a5940ff --- /dev/null +++ b/includes/CacheHelper.php @@ -0,0 +1,363 @@ + + */ +interface ICacheHelper { + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + function setCacheEnabled( $cacheEnabled ); + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + function startCache( $cacheExpiry = null, $cacheEnabled = null ); + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + function getCachedValue( $computeFunction, $args = array(), $key = null ); + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + function saveCache(); + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry.. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + function setExpiry( $cacheExpiry ); + +} + +/** + * Helper class for caching various elements in a single cache entry. + * + * To get a cached value or compute it, use getCachedValue like this: + * $this->getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * Before the first addCachedHTML call, you should call $this->startCache(); + * After adding the last HTML that should be cached, call $this->saveCache(); + * + * @since 1.20 + * + * @file CacheHelper.php + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class CacheHelper implements ICacheHelper { + + /** + * The time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * @var integer + */ + protected $cacheExpiry = 3600; + + /** + * List of HTML chunks to be cached (if !hasCached) or that where cached (of hasCached). + * If not cached already, then the newly computed chunks are added here, + * if it as cached already, chunks are removed from this list as they are needed. + * + * @since 1.20 + * @var array + */ + protected $cachedChunks; + + /** + * Indicates if the to be cached content was already cached. + * Null if this information is not available yet. + * + * @since 1.20 + * @var boolean|null + */ + protected $hasCached = null; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Function that gets called when initialization is done. + * + * @since 1.20 + * @var function + */ + protected $onInitHandler = false; + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheEnabled = $cacheEnabled; + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + if ( is_null( $this->hasCached ) ) { + if ( !is_null( $cacheExpiry ) ) { + $this->cacheExpiry = $cacheExpiry; + } + + if ( !is_null( $cacheEnabled ) ) { + $this->setCacheEnabled( $cacheEnabled ); + } + + $this->initCaching(); + } + } + + /** + * Returns a message that notifies the user he/she is looking at + * a cached version of the page, including a refresh link. + * + * @since 1.20 + * + * @param IContextSource $context + * @param boolean $includePurgeLink + * + * @return string + */ + public function getCachedNotice( IContextSource $context, $includePurgeLink = true ) { + if ( $this->cacheExpiry < 86400 * 3650 ) { + $message = $context->msg( + 'cachedspecial-viewing-cached-ttl', + $context->getLanguage()->formatDuration( $this->cacheExpiry ) + )->escaped(); + } + else { + $message = $context->msg( + 'cachedspecial-viewing-cached-ts' + )->escaped(); + } + + if ( $includePurgeLink ) { + $refreshArgs = $context->getRequest()->getQueryValues(); + unset( $refreshArgs['title'] ); + $refreshArgs['action'] = 'purge'; + + $subPage = $context->getTitle()->getFullText(); + $subPage = explode( '/', $subPage, 2 ); + $subPage = count( $subPage ) > 1 ? $subPage[1] : false; + + $message .= ' ' . Linker::link( + $context->getTitle( $subPage ), + $context->msg( 'cachedspecial-refresh-now' )->escaped(), + array(), + $refreshArgs + ); + } + + return $message; + } + + /** + * Initializes the caching if not already done so. + * Should be called before any of the caching functionality is used. + * + * @since 1.20 + */ + protected function initCaching() { + if ( $this->cacheEnabled && is_null( $this->hasCached ) ) { + $cachedChunks = wfGetCache( CACHE_ANYTHING )->get( $this->getCacheKeyString() ); + + $this->hasCached = is_array( $cachedChunks ); + $this->cachedChunks = $this->hasCached ? $cachedChunks : array(); + + if ( $this->onInitHandler !== false ) { + call_user_func( $this->onInitHandler, $this->hasCached ); + } + } + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + $this->initCaching(); + + if ( $this->cacheEnabled && $this->hasCached ) { + $value = null; + + if ( is_null( $key ) ) { + $itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) ); + $itemKey = array_shift( $itemKey ); + + if ( !is_integer( $itemKey ) ) { + wfWarn( "Attempted to get item with non-numeric key while the next item in the queue has a key ($itemKey) in " . __METHOD__ ); + } + elseif ( is_null( $itemKey ) ) { + wfWarn( "Attempted to get an item while the queue is empty in " . __METHOD__ ); + } + else { + $value = array_shift( $this->cachedChunks ); + } + } + else { + if ( array_key_exists( $key, $this->cachedChunks ) ) { + $value = $this->cachedChunks[$key]; + unset( $this->cachedChunks[$key] ); + } + else { + wfWarn( "There is no item with key '$key' in this->cachedChunks in " . __METHOD__ ); + } + } + } + else { + if ( !is_array( $args ) ) { + $args = array( $args ); + } + + $value = call_user_func_array( $computeFunction, $args ); + + if ( $this->cacheEnabled ) { + if ( is_null( $key ) ) { + $this->cachedChunks[] = $value; + } + else { + $this->cachedChunks[$key] = $value; + } + } + } + + return $value; + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + if ( $this->cacheEnabled && $this->hasCached === false && !empty( $this->cachedChunks ) ) { + wfGetCache( CACHE_ANYTHING )->set( $this->getCacheKeyString(), $this->cachedChunks, $this->cacheExpiry ); + } + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry.. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheExpiry = $cacheExpiry; + } + + /** + * Returns the cache key to use to cache this page's HTML output. + * Is constructed from the special page name and language code. + * + * @since 1.20 + * + * @return string + */ + protected function getCacheKeyString() { + return call_user_func_array( 'wfMemcKey', $this->cacheKey ); + } + + /** + * Sets the cache key that should be used. + * + * @since 1.20 + * + * @param array $cacheKey + */ + public function setCacheKey( array $cacheKey ) { + $this->cacheKey = $cacheKey; + } + + /** + * Rebuild the content, even if it's already cached. + * This effectively has the same effect as purging the cache, + * since it will be overridden with the new value on the next request. + * + * @since 1.20 + */ + public function rebuildOnDemand() { + $this->hasCached = false; + } + + /** + * Sets a function that gets called when initialization of the cache is done. + * + * @since 1.20 + * + * @param $handlerFunction + */ + public function setOnInitializedHandler( $handlerFunction ) { + $this->onInitHandler = $handlerFunction; + } + +} \ No newline at end of file diff --git a/includes/actions/CachedAction.php b/includes/actions/CachedAction.php new file mode 100644 index 0000000000..3f73ea4285 --- /dev/null +++ b/includes/actions/CachedAction.php @@ -0,0 +1,170 @@ +getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * @since 1.20 + * + * @file CachedAction.php + * @ingroup Action + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class CachedAction extends FormlessAction implements ICacheHelper { + + /** + * CacheHelper object to which we forward the non-SpecialPage specific caching work. + * Initialized in startCache. + * + * @since 1.20 + * @var CacheHelper + */ + protected $cacheHelper; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheHelper->setCacheEnabled( $cacheEnabled ); + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + $this->cacheHelper = new CacheHelper(); + + $this->cacheHelper->setCacheEnabled( $this->cacheEnabled ); + $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) ); + + $keyArgs = $this->getCacheKey(); + + if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) { + unset( $keyArgs['action'] ); + } + + $this->cacheHelper->setCacheKey( $keyArgs ); + + if ( $this->getRequest()->getText( 'action' ) === 'purge' ) { + $this->cacheHelper->rebuildOnDemand(); + } + + $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled ); + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ); + } + + /** + * Add some HTML to be cached. + * This is done by providing a callback function that should + * return the HTML to be added. It will only be called if the + * item is not in the cache yet or when the cache has been invalidated. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array $args + * @param string|null $key + */ + public function addCachedHTML( $computeFunction, $args = array(), $key = null ) { + $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) ); + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + $this->cacheHelper->saveCache(); + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheHelper->setExpiry( $cacheExpiry ); + } + + /** + * Returns the variables used to constructed the cache key in an array. + * + * @since 1.20 + * + * @return array + */ + protected function getCacheKey() { + return array( + get_class( $this->page ), + $this->getName(), + $this->getLanguage()->getCode() + ); + } + + /** + * Gets called after the cache got initialized. + * + * @since 1.20 + * + * @param boolean $hasCached + */ + public function onCacheInitialized( $hasCached ) { + if ( $hasCached ) { + $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) ); + } + } + +} \ No newline at end of file diff --git a/includes/specials/SpecialCachedPage.php b/includes/specials/SpecialCachedPage.php new file mode 100644 index 0000000000..7b59b9393d --- /dev/null +++ b/includes/specials/SpecialCachedPage.php @@ -0,0 +1,169 @@ +getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * @since 1.20 + * + * @file SpecialCachedPage.php + * @ingroup SpecialPage + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { + + /** + * CacheHelper object to which we forward the non-SpecialPage specific caching work. + * Initialized in startCache. + * + * @since 1.20 + * @var CacheHelper + */ + protected $cacheHelper; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheHelper->setCacheEnabled( $cacheEnabled ); + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + $this->cacheHelper = new CacheHelper(); + + $this->cacheHelper->setCacheEnabled( $this->cacheEnabled ); + $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) ); + + $keyArgs = $this->getCacheKey(); + + if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) { + unset( $keyArgs['action'] ); + } + + $this->cacheHelper->setCacheKey( $keyArgs ); + + if ( $this->getRequest()->getText( 'action' ) === 'purge' ) { + $this->cacheHelper->rebuildOnDemand(); + } + + $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled ); + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ); + } + + /** + * Add some HTML to be cached. + * This is done by providing a callback function that should + * return the HTML to be added. It will only be called if the + * item is not in the cache yet or when the cache has been invalidated. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array $args + * @param string|null $key + */ + public function addCachedHTML( $computeFunction, $args = array(), $key = null ) { + $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) ); + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + $this->cacheHelper->saveCache(); + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheHelper->setExpiry( $cacheExpiry ); + } + + /** + * Returns the variables used to constructed the cache key in an array. + * + * @since 1.20 + * + * @return array + */ + protected function getCacheKey() { + return array( + $this->mName, + $this->getLanguage()->getCode() + ); + } + + /** + * Gets called after the cache got initialized. + * + * @since 1.20 + * + * @param boolean $hasCached + */ + public function onCacheInitialized( $hasCached ) { + if ( $hasCached ) { + $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) ); + } + } + +} \ No newline at end of file diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 123399cb9d..01b8c14eef 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -2671,6 +2671,11 @@ It may contain one or more characters which cannot be used in titles.', 'allpages-bad-ns' => '{{SITENAME}} does not have namespace "$1".', 'allpages-hide-redirects' => 'Hide redirects', +# SpecialCachedPage +'cachedspecial-viewing-cached-ttl' => 'You are viewing a cached version of this page, which can be up to $1 old.', +'cachedspecial-viewing-cached-ts' => 'You are viewing a cached version of this page, which might not be completely actual.', +'cachedspecial-refresh-now' => 'View latest.', + # Special:Categories 'categories' => 'Categories', 'categories-summary' => '', # do not translate or duplicate this message to other languages diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 58501278c1..f14e42d702 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -4664,5 +4664,7 @@ $4 is the gender of the target user.', * $1 is an unknown warning.', 'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.', 'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.', - +'cachedspecial-viewing-cached-ttl' => 'Message notifying they are watching a cached page. $1 is a duration (ie "1 hour and 30 minutes")', +'cachedspecial-viewing-cached-ts' => 'Message notifying they are watching a cached page.', +'cachedspecial-refresh-now' => 'Link text pointing to the most recent version of the page.', ); diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index d4184eb970..40cfad3f90 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -1755,6 +1755,11 @@ $wgMessageStructure = array( 'allpages-bad-ns', 'allpages-hide-redirects', ), + 'cachedspecial' => array( + 'cachedspecial-viewing-cached-ttl', + 'cachedspecial-viewing-cached-ts', + 'cachedspecial-refresh-now', + ), 'categories' => array( 'categories', 'categories-summary', @@ -3943,4 +3948,5 @@ Variants for Chinese language", 'feedback' => 'Feedback', 'apierrors' => 'API errors', 'duration' => 'Durations', + 'cachedspecial' => 'SpecialCachedPage', ); -- 2.20.1