summaryrefslogtreecommitdiff
path: root/includes/cache
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2015-12-17 09:15:42 +0100
committerPierre Schmitz <pierre@archlinux.de>2015-12-17 09:44:51 +0100
commita1789ddde42033f1b05cc4929491214ee6e79383 (patch)
tree63615735c4ddffaaabf2428946bb26f90899f7bf /includes/cache
parent9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff)
Update to MediaWiki 1.26.0
Diffstat (limited to 'includes/cache')
-rw-r--r--includes/cache/BacklinkCache.php6
-rw-r--r--includes/cache/CacheDependency.php8
-rw-r--r--includes/cache/FileCacheBase.php4
-rw-r--r--includes/cache/HTMLFileCache.php1
-rw-r--r--includes/cache/LCStoreStaticArray.php140
-rw-r--r--includes/cache/LinkBatch.php2
-rw-r--r--includes/cache/LinkCache.php87
-rw-r--r--includes/cache/LocalisationCache.php13
-rw-r--r--includes/cache/MessageBlobStore.php425
-rw-r--r--includes/cache/MessageCache.php557
-rw-r--r--includes/cache/ResourceFileCache.php4
-rw-r--r--includes/cache/UserCache.php4
12 files changed, 938 insertions, 313 deletions
diff --git a/includes/cache/BacklinkCache.php b/includes/cache/BacklinkCache.php
index 10b4fb00..1296c136 100644
--- a/includes/cache/BacklinkCache.php
+++ b/includes/cache/BacklinkCache.php
@@ -157,7 +157,7 @@ class BacklinkCache {
* @param string $table
* @param int|bool $startId
* @param int|bool $endId
- * @param int|INF $max
+ * @param int $max
* @return TitleArrayFromResult
*/
public function getLinks( $table, $startId = false, $endId = false, $max = INF ) {
@@ -169,7 +169,7 @@ class BacklinkCache {
* @param string $table
* @param int|bool $startId
* @param int|bool $endId
- * @param int|INF $max
+ * @param int $max
* @param string $select 'all' or 'ids'
* @return ResultWrapper
*/
@@ -319,7 +319,7 @@ class BacklinkCache {
/**
* Get the approximate number of backlinks
* @param string $table
- * @param int|INF $max Only count up to this many backlinks
+ * @param int $max Only count up to this many backlinks
* @return int
*/
public function getNumLinks( $table, $max = INF ) {
diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php
index 517f3798..2abcabdc 100644
--- a/includes/cache/CacheDependency.php
+++ b/includes/cache/CacheDependency.php
@@ -181,11 +181,11 @@ class FileDependency extends CacheDependency {
function loadDependencyValues() {
if ( is_null( $this->timestamp ) ) {
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
# Dependency on a non-existent file stores "false"
# This is a valid concept!
$this->timestamp = filemtime( $this->filename );
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
}
}
@@ -193,9 +193,9 @@ class FileDependency extends CacheDependency {
* @return bool
*/
function isExpired() {
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
$lastmod = filemtime( $this->filename );
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
if ( $lastmod === false ) {
if ( $this->timestamp === false ) {
# Still nonexistent
diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php
index 4bf36114..5632596a 100644
--- a/includes/cache/FileCacheBase.php
+++ b/includes/cache/FileCacheBase.php
@@ -185,9 +185,9 @@ abstract class FileCacheBase {
* @return void
*/
public function clearCache() {
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
unlink( $this->cachePath() );
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
$this->mCached = false;
}
diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php
index c07032bf..483eaa57 100644
--- a/includes/cache/HTMLFileCache.php
+++ b/includes/cache/HTMLFileCache.php
@@ -48,6 +48,7 @@ class HTMLFileCache extends FileCacheBase {
* @throws MWException
*/
public function __construct( $title, $action ) {
+ parent::__construct();
$allowedTypes = self::cacheablePageActions();
if ( !in_array( $action, $allowedTypes ) ) {
throw new MWException( 'Invalid file cache type given.' );
diff --git a/includes/cache/LCStoreStaticArray.php b/includes/cache/LCStoreStaticArray.php
new file mode 100644
index 00000000..fff9bab2
--- /dev/null
+++ b/includes/cache/LCStoreStaticArray.php
@@ -0,0 +1,140 @@
+<?php
+/**
+ * Localisation cache storage based on PHP files and static arrays.
+ *
+ * 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
+ */
+
+/**
+ * @since 1.26
+ */
+class LCStoreStaticArray implements LCStore {
+ /** @var string|null Current language code. */
+ private $currentLang = null;
+
+ /** @var array Localisation data. */
+ private $data = array();
+
+ /** @var string File name. */
+ private $fname = null;
+
+ /** @var string Directory for cache files. */
+ private $directory;
+
+ public function __construct( $conf = array() ) {
+ global $wgCacheDirectory;
+
+ if ( isset( $conf['directory'] ) ) {
+ $this->directory = $conf['directory'];
+ } else {
+ $this->directory = $wgCacheDirectory;
+ }
+ }
+
+ public function startWrite( $code ) {
+ $this->currentLang = $code;
+ $this->fname = $this->directory . '/' . $code . '.l10n.php';
+ $this->data[$code] = array();
+ if ( file_exists( $this->fname ) ) {
+ $this->data[$code] = require $this->fname;
+ }
+ }
+
+ public function set( $key, $value ) {
+ $this->data[$this->currentLang][$key] = self::encode( $value );
+ }
+
+ /**
+ * Encodes a value into an array format
+ *
+ * @param mixed $value
+ * @return array
+ * @throws RuntimeException
+ */
+ public static function encode( $value ) {
+ if ( is_scalar( $value ) || $value === null ) {
+ // [V]alue
+ return array( 'v', $value );
+ }
+ if ( is_object( $value ) ) {
+ // [S]erialized
+ return array( 's', serialize( $value ) );
+ }
+ if ( is_array( $value ) ) {
+ // [A]rray
+ return array( 'a', array_map( function ( $v ) {
+ return LCStoreStaticArray::encode( $v );
+ }, $value ) );
+ }
+
+ throw new RuntimeException( 'Cannot encode ' . var_export( $value, true ) );
+ }
+
+ /**
+ * Decode something that was encoded with encode
+ *
+ * @param array $encoded
+ * @return array|mixed
+ * @throws RuntimeException
+ */
+ public static function decode( array $encoded ) {
+ $type = $encoded[0];
+ $data = $encoded[1];
+
+ switch ( $type ) {
+ case 'v':
+ return $data;
+ case 's':
+ return unserialize( $data );
+ case 'a':
+ return array_map( function ( $v ) {
+ return LCStoreStaticArray::decode( $v );
+ }, $data );
+ default:
+ throw new RuntimeException(
+ 'Unable to decode ' . var_export( $encoded, true ) );
+ }
+ }
+
+ public function finishWrite() {
+ file_put_contents(
+ $this->fname,
+ "<?php\n" .
+ "// Generated by LCStoreStaticArray.php -- do not edit!\n" .
+ "return " .
+ var_export( $this->data[$this->currentLang], true ) . ';'
+ );
+ $this->currentLang = null;
+ $this->fname = null;
+ }
+
+ public function get( $code, $key ) {
+ if ( !array_key_exists( $code, $this->data ) ) {
+ $fname = $this->directory . '/' . $code . '.l10n.php';
+ if ( !file_exists( $fname ) ) {
+ return null;
+ }
+ $this->data[$code] = require $fname;
+ }
+ $data = $this->data[$code];
+ if ( array_key_exists( $key, $data ) ) {
+ return self::decode( $data[$key] );
+ }
+ return null;
+ }
+}
diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php
index 77e4d490..698b3046 100644
--- a/includes/cache/LinkBatch.php
+++ b/includes/cache/LinkBatch.php
@@ -78,7 +78,7 @@ class LinkBatch {
$this->data[$ns] = array();
}
- $this->data[$ns][str_replace( ' ', '_', $dbkey )] = 1;
+ $this->data[$ns][strtr( $dbkey, ' ', '_' )] = 1;
}
/**
diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php
index eace1eea..56c92569 100644
--- a/includes/cache/LinkCache.php
+++ b/includes/cache/LinkCache.php
@@ -29,18 +29,34 @@
class LinkCache {
// Increment $mClassVer whenever old serialized versions of this class
// becomes incompatible with the new version.
- private $mClassVer = 4;
+ private $mClassVer = 5;
- private $mGoodLinks = array();
- private $mGoodLinkFields = array();
- private $mBadLinks = array();
+ /**
+ * @var MapCacheLRU
+ */
+ private $mGoodLinks;
+ /**
+ * @var MapCacheLRU
+ */
+ private $mBadLinks;
private $mForUpdate = false;
/**
+ * How many Titles to store. There are two caches, so the amount actually
+ * stored in memory can be up to twice this.
+ */
+ const MAX_SIZE = 10000;
+
+ /**
* @var LinkCache
*/
protected static $instance;
+ public function __construct() {
+ $this->mGoodLinks = new MapCacheLRU( self::MAX_SIZE );
+ $this->mBadLinks = new MapCacheLRU( self::MAX_SIZE );
+ }
+
/**
* Get an instance of this class.
*
@@ -90,8 +106,9 @@ class LinkCache {
* @return int
*/
public function getGoodLinkID( $title ) {
- if ( array_key_exists( $title, $this->mGoodLinks ) ) {
- return $this->mGoodLinks[$title];
+ if ( $this->mGoodLinks->has( $title ) ) {
+ $info = $this->mGoodLinks->get( $title );
+ return $info['id'];
} else {
return 0;
}
@@ -106,8 +123,9 @@ class LinkCache {
*/
public function getGoodLinkFieldObj( $title, $field ) {
$dbkey = $title->getPrefixedDBkey();
- if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) {
- return $this->mGoodLinkFields[$dbkey][$field];
+ if ( $this->mGoodLinks->has( $dbkey ) ) {
+ $info = $this->mGoodLinks->get( $dbkey );
+ return $info[$field];
} else {
return null;
}
@@ -118,7 +136,8 @@ class LinkCache {
* @return bool
*/
public function isBadLink( $title ) {
- return array_key_exists( $title, $this->mBadLinks );
+ // We need to use get here since has will not call ping.
+ return $this->mBadLinks->get( $title ) !== null;
}
/**
@@ -135,13 +154,13 @@ class LinkCache {
$revision = 0, $model = null
) {
$dbkey = $title->getPrefixedDBkey();
- $this->mGoodLinks[$dbkey] = (int)$id;
- $this->mGoodLinkFields[$dbkey] = array(
+ $this->mGoodLinks->set( $dbkey, array(
+ 'id' => (int)$id,
'length' => (int)$len,
'redirect' => (int)$redir,
'revision' => (int)$revision,
'model' => $model ? (string)$model : null,
- );
+ ) );
}
/**
@@ -153,13 +172,13 @@ class LinkCache {
*/
public function addGoodLinkObjFromRow( $title, $row ) {
$dbkey = $title->getPrefixedDBkey();
- $this->mGoodLinks[$dbkey] = intval( $row->page_id );
- $this->mGoodLinkFields[$dbkey] = array(
+ $this->mGoodLinks->set( $dbkey, array(
+ 'id' => intval( $row->page_id ),
'length' => intval( $row->page_len ),
'redirect' => intval( $row->page_is_redirect ),
'revision' => intval( $row->page_latest ),
'model' => !empty( $row->page_content_model ) ? strval( $row->page_content_model ) : null,
- );
+ ) );
}
/**
@@ -168,12 +187,12 @@ class LinkCache {
public function addBadLinkObj( $title ) {
$dbkey = $title->getPrefixedDBkey();
if ( !$this->isBadLink( $dbkey ) ) {
- $this->mBadLinks[$dbkey] = 1;
+ $this->mBadLinks->set( $dbkey, 1 );
}
}
public function clearBadLink( $title ) {
- unset( $this->mBadLinks[$title] );
+ $this->mBadLinks->clear( array( $title ) );
}
/**
@@ -181,17 +200,33 @@ class LinkCache {
*/
public function clearLink( $title ) {
$dbkey = $title->getPrefixedDBkey();
- unset( $this->mBadLinks[$dbkey] );
- unset( $this->mGoodLinks[$dbkey] );
- unset( $this->mGoodLinkFields[$dbkey] );
+ $this->mBadLinks->clear( array( $dbkey ) );
+ $this->mGoodLinks->clear( array( $dbkey ) );
}
+
+ /**
+ * @deprecated since 1.26
+ * @return array
+ */
public function getGoodLinks() {
- return $this->mGoodLinks;
+ wfDeprecated( __METHOD__, '1.26' );
+ $links = array();
+ foreach ( $this->mGoodLinks->getAllKeys() as $key ) {
+ $info = $this->mGoodLinks->get( $key );
+ $links[$key] = $info['id'];
+ }
+
+ return $links;
}
+ /**
+ * @deprecated since 1.26
+ * @return array
+ */
public function getBadLinks() {
- return array_keys( $this->mBadLinks );
+ wfDeprecated( __METHOD__, '1.26' );
+ return $this->mBadLinks->getAllKeys();
}
/**
@@ -220,17 +255,14 @@ class LinkCache {
$key = $nt->getPrefixedDBkey();
if ( $this->isBadLink( $key ) || $nt->isExternal() ) {
-
return 0;
}
$id = $this->getGoodLinkID( $key );
if ( $id != 0 ) {
-
return $id;
}
if ( $key === '' ) {
-
return 0;
}
@@ -265,8 +297,7 @@ class LinkCache {
* Clears cache
*/
public function clear() {
- $this->mGoodLinks = array();
- $this->mGoodLinkFields = array();
- $this->mBadLinks = array();
+ $this->mGoodLinks->clear();
+ $this->mBadLinks->clear();
}
}
diff --git a/includes/cache/LocalisationCache.php b/includes/cache/LocalisationCache.php
index dc5a2eb6..276e84aa 100644
--- a/includes/cache/LocalisationCache.php
+++ b/includes/cache/LocalisationCache.php
@@ -204,6 +204,9 @@ class LocalisationCache {
case 'db':
$storeClass = 'LCStoreDB';
break;
+ case 'array':
+ $storeClass = 'LCStoreStaticArray';
+ break;
case 'detect':
$storeClass = $wgCacheDirectory ? 'LCStoreCDB' : 'LCStoreDB';
break;
@@ -506,15 +509,15 @@ class LocalisationCache {
*/
protected function readPHPFile( $_fileName, $_fileType ) {
// Disable APC caching
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
$_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
include $_fileName;
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
ini_set( 'apc.cache_by_default', $_apcEnabled );
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
if ( $_fileType == 'core' || $_fileType == 'extension' ) {
$data = compact( self::$allKeys );
@@ -536,13 +539,11 @@ class LocalisationCache {
public function readJSONFile( $fileName ) {
if ( !is_readable( $fileName ) ) {
-
return array();
}
$json = file_get_contents( $fileName );
if ( $json === false ) {
-
return array();
}
diff --git a/includes/cache/MessageBlobStore.php b/includes/cache/MessageBlobStore.php
new file mode 100644
index 00000000..19349b2d
--- /dev/null
+++ b/includes/cache/MessageBlobStore.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * Resource message blobs storage used by the resource loader.
+ *
+ * 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
+ * @author Roan Kattouw
+ * @author Trevor Parscal
+ */
+
+/**
+ * This class provides access to the resource message blobs storage used by
+ * the ResourceLoader.
+ *
+ * A message blob is a JSON object containing the interface messages for a
+ * certain resource in a certain language. These message blobs are cached
+ * in the msg_resource table and automatically invalidated when one of their
+ * constituent messages or the resource itself is changed.
+ */
+class MessageBlobStore {
+ /**
+ * In-process cache for message blobs.
+ *
+ * Keyed by language code, then module name.
+ *
+ * @var array
+ */
+ protected $blobCache = array();
+
+ /**
+ * Get the singleton instance
+ *
+ * @since 1.24
+ * @deprecated since 1.25
+ * @return MessageBlobStore
+ */
+ public static function getInstance() {
+ wfDeprecated( __METHOD__, '1.25' );
+ return new self;
+ }
+
+ /**
+ * Get the message blobs for a set of modules
+ *
+ * @param ResourceLoader $resourceLoader
+ * @param array $modules Array of module objects keyed by module name
+ * @param string $lang Language code
+ * @return array An array mapping module names to message blobs
+ */
+ public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
+ if ( !count( $modules ) ) {
+ return array();
+ }
+
+ $blobs = array();
+
+ // Try in-process cache
+ $missingFromCache = array();
+ foreach ( $modules as $name => $module ) {
+ if ( isset( $this->blobCache[$lang][$name] ) ) {
+ $blobs[$name] = $this->blobCache[$lang][$name];
+ } else {
+ $missingFromCache[] = $name;
+ }
+ }
+
+ // Try DB cache
+ if ( $missingFromCache ) {
+ $blobs += $this->getFromDB( $resourceLoader, $missingFromCache, $lang );
+ }
+
+ // Generate new blobs for any remaining modules and store in DB
+ $missingFromDb = array_diff( array_keys( $modules ), array_keys( $blobs ) );
+ foreach ( $missingFromDb as $name ) {
+ $blob = $this->insertMessageBlob( $name, $modules[$name], $lang );
+ if ( $blob ) {
+ $blobs[$name] = $blob;
+ }
+ }
+
+ // Update in-process cache
+ if ( isset( $this->blobCache[$lang] ) ) {
+ $this->blobCache[$lang] += $blobs;
+ } else {
+ $this->blobCache[$lang] = $blobs;
+ }
+
+ return $blobs;
+ }
+
+ /**
+ * Generate and insert a new message blob. If the blob was already
+ * present, it is not regenerated; instead, the preexisting blob
+ * is fetched and returned.
+ *
+ * @param string $name Module name
+ * @param ResourceLoaderModule $module
+ * @param string $lang Language code
+ * @return mixed Message blob or false if the module has no messages
+ */
+ public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
+ $blob = $this->generateMessageBlob( $module, $lang );
+
+ if ( !$blob ) {
+ return false;
+ }
+
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+ $success = $dbw->insert( 'msg_resource', array(
+ 'mr_lang' => $lang,
+ 'mr_resource' => $name,
+ 'mr_blob' => $blob,
+ 'mr_timestamp' => $dbw->timestamp()
+ ),
+ __METHOD__,
+ array( 'IGNORE' )
+ );
+
+ if ( $success ) {
+ if ( $dbw->affectedRows() == 0 ) {
+ // Blob was already present, fetch it
+ $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array(
+ 'mr_resource' => $name,
+ 'mr_lang' => $lang,
+ ),
+ __METHOD__
+ );
+ } else {
+ // Update msg_resource_links
+ $rows = array();
+
+ foreach ( $module->getMessages() as $key ) {
+ $rows[] = array(
+ 'mrl_resource' => $name,
+ 'mrl_message' => $key
+ );
+ }
+ $dbw->insert( 'msg_resource_links', $rows,
+ __METHOD__, array( 'IGNORE' )
+ );
+ }
+ }
+ } catch ( DBError $e ) {
+ wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ }
+ return $blob;
+ }
+
+ /**
+ * Update the message blob for a given module in a given language
+ *
+ * @param string $name Module name
+ * @param ResourceLoaderModule $module
+ * @param string $lang Language code
+ * @return string Regenerated message blob, or null if there was no blob for
+ * the given module/language pair.
+ */
+ public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $row = $dbw->selectRow( 'msg_resource', 'mr_blob',
+ array( 'mr_resource' => $name, 'mr_lang' => $lang ),
+ __METHOD__
+ );
+ if ( !$row ) {
+ return null;
+ }
+
+ // Save the old and new blobs for later
+ $oldBlob = $row->mr_blob;
+ $newBlob = $this->generateMessageBlob( $module, $lang );
+
+ try {
+ $newRow = array(
+ 'mr_resource' => $name,
+ 'mr_lang' => $lang,
+ 'mr_blob' => $newBlob,
+ 'mr_timestamp' => $dbw->timestamp()
+ );
+
+ $dbw->replace( 'msg_resource',
+ array( array( 'mr_resource', 'mr_lang' ) ),
+ $newRow, __METHOD__
+ );
+
+ // Figure out which messages were added and removed
+ $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) );
+ $newMessages = array_keys( FormatJson::decode( $newBlob, true ) );
+ $added = array_diff( $newMessages, $oldMessages );
+ $removed = array_diff( $oldMessages, $newMessages );
+
+ // Delete removed messages, insert added ones
+ if ( $removed ) {
+ $dbw->delete( 'msg_resource_links', array(
+ 'mrl_resource' => $name,
+ 'mrl_message' => $removed
+ ), __METHOD__
+ );
+ }
+
+ $newLinksRows = array();
+
+ foreach ( $added as $message ) {
+ $newLinksRows[] = array(
+ 'mrl_resource' => $name,
+ 'mrl_message' => $message
+ );
+ }
+
+ if ( $newLinksRows ) {
+ $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__,
+ array( 'IGNORE' ) // just in case
+ );
+ }
+ } catch ( Exception $e ) {
+ wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ }
+ return $newBlob;
+ }
+
+ /**
+ * Update a single message in all message blobs it occurs in.
+ *
+ * @param string $key Message key
+ */
+ public function updateMessage( $key ) {
+ try {
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Keep running until the updates queue is empty.
+ // Due to update conflicts, the queue might not be emptied
+ // in one iteration.
+ $updates = null;
+ do {
+ $updates = $this->getUpdatesForMessage( $key, $updates );
+
+ foreach ( $updates as $k => $update ) {
+ // Update the row on the condition that it
+ // didn't change since we fetched it by putting
+ // the timestamp in the WHERE clause.
+ $success = $dbw->update( 'msg_resource',
+ array(
+ 'mr_blob' => $update['newBlob'],
+ 'mr_timestamp' => $dbw->timestamp() ),
+ array(
+ 'mr_resource' => $update['resource'],
+ 'mr_lang' => $update['lang'],
+ 'mr_timestamp' => $update['timestamp'] ),
+ __METHOD__
+ );
+
+ // Only requeue conflicted updates.
+ // If update() returned false, don't retry, for
+ // fear of getting into an infinite loop
+ if ( !( $success && $dbw->affectedRows() == 0 ) ) {
+ // Not conflicted
+ unset( $updates[$k] );
+ }
+ }
+ } while ( count( $updates ) );
+
+ // No need to update msg_resource_links because we didn't add
+ // or remove any messages, we just changed their contents.
+ } catch ( Exception $e ) {
+ wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ }
+ }
+
+ public function clear() {
+ // TODO: Give this some more thought
+ try {
+ // Not using TRUNCATE, because that needs extra permissions,
+ // which maybe not granted to the database user.
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'msg_resource', '*', __METHOD__ );
+ $dbw->delete( 'msg_resource_links', '*', __METHOD__ );
+ } catch ( Exception $e ) {
+ wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ }
+ }
+
+ /**
+ * Create an update queue for updateMessage()
+ *
+ * @param string $key Message key
+ * @param array $prevUpdates Updates queue to refresh or null to build a fresh update queue
+ * @return array Updates queue
+ */
+ private function getUpdatesForMessage( $key, $prevUpdates = null ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ if ( is_null( $prevUpdates ) ) {
+ // Fetch all blobs referencing $key
+ $res = $dbw->select(
+ array( 'msg_resource', 'msg_resource_links' ),
+ array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
+ array( 'mrl_message' => $key, 'mr_resource=mrl_resource' ),
+ __METHOD__
+ );
+ } else {
+ // Refetch the blobs referenced by $prevUpdates
+
+ // Reorganize the (resource, lang) pairs in the format
+ // expected by makeWhereFrom2d()
+ $twoD = array();
+
+ foreach ( $prevUpdates as $update ) {
+ $twoD[$update['resource']][$update['lang']] = true;
+ }
+
+ $res = $dbw->select( 'msg_resource',
+ array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
+ $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ),
+ __METHOD__
+ );
+ }
+
+ // Build the new updates queue
+ $updates = array();
+
+ foreach ( $res as $row ) {
+ $updates[] = array(
+ 'resource' => $row->mr_resource,
+ 'lang' => $row->mr_lang,
+ 'timestamp' => $row->mr_timestamp,
+ 'newBlob' => $this->reencodeBlob( $row->mr_blob, $key, $row->mr_lang )
+ );
+ }
+
+ return $updates;
+ }
+
+ /**
+ * Reencode a message blob with the updated value for a message
+ *
+ * @param string $blob Message blob (JSON object)
+ * @param string $key Message key
+ * @param string $lang Language code
+ * @return string Message blob with $key replaced with its new value
+ */
+ private function reencodeBlob( $blob, $key, $lang ) {
+ $decoded = FormatJson::decode( $blob, true );
+ $decoded[$key] = wfMessage( $key )->inLanguage( $lang )->plain();
+
+ return FormatJson::encode( (object)$decoded );
+ }
+
+ /**
+ * Get the message blobs for a set of modules from the database.
+ * Modules whose blobs are not in the database are silently dropped.
+ *
+ * @param ResourceLoader $resourceLoader
+ * @param array $modules Array of module names
+ * @param string $lang Language code
+ * @throws MWException
+ * @return array Array mapping module names to blobs
+ */
+ private function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) {
+ if ( !count( $modules ) ) {
+ return array();
+ }
+
+ $config = $resourceLoader->getConfig();
+ $retval = array();
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'msg_resource',
+ array( 'mr_blob', 'mr_resource', 'mr_timestamp' ),
+ array( 'mr_resource' => $modules, 'mr_lang' => $lang ),
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $module = $resourceLoader->getModule( $row->mr_resource );
+ if ( !$module ) {
+ // This shouldn't be possible
+ throw new MWException( __METHOD__ . ' passed an invalid module name' );
+ }
+
+ // Update the module's blobs if the set of messages changed or if the blob is
+ // older than the CacheEpoch setting
+ $keys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
+ $values = array_values( array_unique( $module->getMessages() ) );
+ if ( $keys !== $values
+ || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $config->get( 'CacheEpoch' )
+ ) {
+ $retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang );
+ } else {
+ $retval[$row->mr_resource] = $row->mr_blob;
+ }
+ }
+
+ return $retval;
+ }
+
+ /**
+ * Generate the message blob for a given module in a given language.
+ *
+ * @param ResourceLoaderModule $module
+ * @param string $lang Language code
+ * @return string JSON object
+ */
+ private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
+ $messages = array();
+
+ foreach ( $module->getMessages() as $key ) {
+ $messages[$key] = wfMessage( $key )->inLanguage( $lang )->plain();
+ }
+
+ return FormatJson::encode( (object)$messages );
+ }
+}
diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php
index a55e25a3..f22c860a 100644
--- a/includes/cache/MessageCache.php
+++ b/includes/cache/MessageCache.php
@@ -25,24 +25,7 @@
* MediaWiki message cache structure version.
* Bump this whenever the message cache format has changed.
*/
-define( 'MSG_CACHE_VERSION', 1 );
-
-/**
- * Memcached timeout when loading a key.
- * See MessageCache::load()
- */
-define( 'MSG_LOAD_TIMEOUT', 60 );
-
-/**
- * Memcached timeout when locking a key for a writing operation.
- * See MessageCache::lock()
- */
-define( 'MSG_LOCK_TIMEOUT', 30 );
-/**
- * Number of times we will try to acquire a lock from Memcached.
- * This comes in addition to MSG_LOCK_TIMEOUT.
- */
-define( 'MSG_WAIT_TIMEOUT', 30 );
+define( 'MSG_CACHE_VERSION', 2 );
/**
* Message cache
@@ -50,12 +33,20 @@ define( 'MSG_WAIT_TIMEOUT', 30 );
* @ingroup Cache
*/
class MessageCache {
+ const FOR_UPDATE = 1; // force message reload
+
+ /** How long to wait for memcached locks */
+ const WAIT_SEC = 15;
+ /** How long memcached locks last */
+ const LOCK_TTL = 30;
+
/**
* Process local cache of loaded messages that are defined in
* MediaWiki namespace. First array level is a language code,
* second level is message key and the values are either message
* content prefixed with space, or !NONEXISTENT for negative
* caching.
+ * @var array $mCache
*/
protected $mCache;
@@ -84,6 +75,16 @@ class MessageCache {
protected $mLoadedLanguages = array();
/**
+ * @var bool $mInParser
+ */
+ protected $mInParser = false;
+
+ /** @var BagOStuff */
+ protected $mMemc;
+ /** @var WANObjectCache */
+ protected $wanCache;
+
+ /**
* Singleton instance
*
* @var MessageCache $instance
@@ -91,11 +92,6 @@ class MessageCache {
private static $instance;
/**
- * @var bool $mInParser
- */
- protected $mInParser = false;
-
- /**
* Get the signleton instance of this class
*
* @since 1.18
@@ -124,11 +120,31 @@ class MessageCache {
}
/**
+ * Normalize message key input
+ *
+ * @param string $key Input message key to be normalized
+ * @return string Normalized message key
+ */
+ public static function normalizeKey( $key ) {
+ global $wgContLang;
+ $lckey = strtr( $key, ' ', '_' );
+ if ( ord( $lckey ) < 128 ) {
+ $lckey[0] = strtolower( $lckey[0] );
+ } else {
+ $lckey = $wgContLang->lcfirst( $lckey );
+ }
+
+ return $lckey;
+ }
+
+ /**
* @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE.
* @param bool $useDB
* @param int $expiry Lifetime for cache. @see $mExpiry.
*/
function __construct( $memCached, $useDB, $expiry ) {
+ global $wgUseLocalMessageCache;
+
if ( !$memCached ) {
$memCached = wfGetCache( CACHE_NONE );
}
@@ -136,6 +152,14 @@ class MessageCache {
$this->mMemc = $memCached;
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
+
+ if ( $wgUseLocalMessageCache ) {
+ $this->localCache = ObjectCache::newAccelerator( CACHE_NONE );
+ } else {
+ $this->localCache = wfGetCache( CACHE_NONE );
+ }
+
+ $this->wanCache = ObjectCache::getMainWANInstance();
}
/**
@@ -153,70 +177,26 @@ class MessageCache {
}
/**
- * Try to load the cache from a local file.
+ * Try to load the cache from APC.
*
- * @param string $hash The hash of contents, to check validity.
* @param string $code Optional language code, see documenation of load().
- * @return array The cache array
+ * @return array|bool The cache array, or false if not in cache.
*/
- function getLocalCache( $hash, $code ) {
- global $wgCacheDirectory;
-
- $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
-
- # Check file existence
- wfSuppressWarnings();
- $file = fopen( $filename, 'r' );
- wfRestoreWarnings();
- if ( !$file ) {
- return false; // No cache file
- }
-
- // Check to see if the file has the hash specified
- $localHash = fread( $file, 32 );
- if ( $hash === $localHash ) {
- // All good, get the rest of it
- $serialized = '';
- while ( !feof( $file ) ) {
- $serialized .= fread( $file, 100000 );
- }
- fclose( $file );
-
- return unserialize( $serialized );
- } else {
- fclose( $file );
+ protected function getLocalCache( $code ) {
+ $cacheKey = wfMemcKey( __CLASS__, $code );
- return false; // Wrong hash
- }
+ return $this->localCache->get( $cacheKey );
}
/**
- * Save the cache to a local file.
- * @param string $serialized
- * @param string $hash
+ * Save the cache to APC.
+ *
* @param string $code
+ * @param array $cache The cache array
*/
- function saveToLocal( $serialized, $hash, $code ) {
- global $wgCacheDirectory;
-
- $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
- wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail
-
- wfSuppressWarnings();
- $file = fopen( $filename, 'w' );
- wfRestoreWarnings();
-
- if ( !$file ) {
- wfDebug( "Unable to open local cache file for writing\n" );
-
- return;
- }
-
- fwrite( $file, $hash . $serialized );
- fclose( $file );
- wfSuppressWarnings();
- chmod( $filename, 0666 );
- wfRestoreWarnings();
+ protected function saveToLocalCache( $code, $cache ) {
+ $cacheKey = wfMemcKey( __CLASS__, $code );
+ $this->localCache->set( $cacheKey, $cache );
}
/**
@@ -236,12 +216,11 @@ class MessageCache {
* is disabled.
*
* @param bool|string $code Language to which load messages
+ * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
* @throws MWException
* @return bool
*/
- function load( $code = false ) {
- global $wgUseLocalMessageCache;
-
+ function load( $code = false, $mode = null ) {
if ( !is_string( $code ) ) {
# This isn't really nice, so at least make a note about it and try to
# fall back
@@ -250,7 +229,7 @@ class MessageCache {
}
# Don't do double loading...
- if ( isset( $this->mLoadedLanguages[$code] ) ) {
+ if ( isset( $this->mLoadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
return true;
}
@@ -269,45 +248,60 @@ class MessageCache {
$success = false; # Keep track of success
$staleCache = false; # a cache array with expired data, or false if none has been loaded
$where = array(); # Debug info, delayed to avoid spamming debug log too much
- $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
- # Local cache
- # Hash of the contents is stored in memcache, to detect if local cache goes
- # out of date (e.g. due to replace() on some other server)
- if ( $wgUseLocalMessageCache ) {
-
- $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
- if ( $hash ) {
- $cache = $this->getLocalCache( $hash, $code );
- if ( !$cache ) {
- $where[] = 'local cache is empty or has the wrong hash';
- } elseif ( $this->isCacheExpired( $cache ) ) {
- $where[] = 'local cache is expired';
- $staleCache = $cache;
- } else {
- $where[] = 'got from local cache';
- $success = true;
- $this->mCache[$code] = $cache;
- }
- }
+ # Hash of the contents is stored in memcache, to detect if data-center cache
+ # or local cache goes out of date (e.g. due to replace() on some other server)
+ list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
+
+ # Try the local cache and check against the cluster hash key...
+ $cache = $this->getLocalCache( $code );
+ if ( !$cache ) {
+ $where[] = 'local cache is empty';
+ } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
+ $where[] = 'local cache has the wrong hash';
+ $staleCache = $cache;
+ } elseif ( $this->isCacheExpired( $cache ) ) {
+ $where[] = 'local cache is expired';
+ $staleCache = $cache;
+ } elseif ( $hashVolatile ) {
+ $where[] = 'local cache validation key is expired/volatile';
+ $staleCache = $cache;
+ } else {
+ $where[] = 'got from local cache';
+ $success = true;
+ $this->mCache[$code] = $cache;
}
if ( !$success ) {
+ $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
# Try the global cache. If it is empty, try to acquire a lock. If
# the lock can't be acquired, wait for the other thread to finish
# and then try the global cache a second time.
- for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) {
- $cache = $this->mMemc->get( $cacheKey );
- if ( !$cache ) {
- $where[] = 'global cache is empty';
- } elseif ( $this->isCacheExpired( $cache ) ) {
- $where[] = 'global cache is expired';
- $staleCache = $cache;
+ for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
+ if ( $hashVolatile && $staleCache ) {
+ # Do not bother fetching the whole cache blob to avoid I/O.
+ # Instead, just try to get the non-blocking $statusKey lock
+ # below, and use the local stale value if it was not acquired.
+ $where[] = 'global cache is presumed expired';
} else {
- $where[] = 'got from global cache';
- $this->mCache[$code] = $cache;
- $this->saveToCaches( $cache, 'local-only', $code );
- $success = true;
+ $cache = $this->mMemc->get( $cacheKey );
+ if ( !$cache ) {
+ $where[] = 'global cache is empty';
+ } elseif ( $this->isCacheExpired( $cache ) ) {
+ $where[] = 'global cache is expired';
+ $staleCache = $cache;
+ } elseif ( $hashVolatile ) {
+ # DB results are slave lag prone until the holdoff TTL passes.
+ # By then, updates should be reflected in loadFromDBWithLock().
+ # One thread renerates the cache while others use old values.
+ $where[] = 'global cache is expired/volatile';
+ $staleCache = $cache;
+ } else {
+ $where[] = 'got from global cache';
+ $this->mCache[$code] = $cache;
+ $this->saveToCaches( $cache, 'local-only', $code );
+ $success = true;
+ }
}
if ( $success ) {
@@ -315,68 +309,12 @@ class MessageCache {
break;
}
- # We need to call loadFromDB. Limit the concurrency to a single
- # process. This prevents the site from going down when the cache
- # expires.
- $statusKey = wfMemcKey( 'messages', $code, 'status' );
- $acquired = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
- if ( $acquired ) {
- # Unlock the status key if there is an exception
- $that = $this;
- $statusUnlocker = new ScopedCallback( function () use ( $that, $statusKey ) {
- $that->mMemc->delete( $statusKey );
- } );
-
- # Now let's regenerate
- $where[] = 'loading from database';
-
- # Lock the cache to prevent conflicting writes
- # If this lock fails, it doesn't really matter, it just means the
- # write is potentially non-atomic, e.g. the results of a replace()
- # may be discarded.
- if ( $this->lock( $cacheKey ) ) {
- $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) {
- $that->unlock( $cacheKey );
- } );
- } else {
- $mainUnlocker = null;
- $where[] = 'could not acquire main lock';
- }
-
- $cache = $this->loadFromDB( $code );
- $this->mCache[$code] = $cache;
+ # We need to call loadFromDB. Limit the concurrency to one process.
+ # This prevents the site from going down when the cache expires.
+ # Note that the DB slam protection lock here is non-blocking.
+ $loadStatus = $this->loadFromDBWithLock( $code, $where, $mode );
+ if ( $loadStatus === true ) {
$success = true;
- $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
-
- # Unlock
- ScopedCallback::consume( $mainUnlocker );
- ScopedCallback::consume( $statusUnlocker );
-
- if ( !$saveSuccess ) {
- # Cache save has failed.
- # There are two main scenarios where this could be a problem:
- #
- # - The cache is more than the maximum size (typically
- # 1MB compressed).
- #
- # - Memcached has no space remaining in the relevant slab
- # class. This is unlikely with recent versions of
- # memcached.
- #
- # Either way, if there is a local cache, nothing bad will
- # happen. If there is no local cache, disabling the message
- # cache for all requests avoids incurring a loadFromDB()
- # overhead on every request, and thus saves the wiki from
- # complete downtime under moderate traffic conditions.
- if ( !$wgUseLocalMessageCache ) {
- $this->mMemc->set( $statusKey, 'error', 60 * 5 );
- $where[] = 'could not save cache, disabled globally for 5 minutes';
- } else {
- $where[] = "could not save global cache";
- }
- }
-
- # Load from DB complete, no need to retry
break;
} elseif ( $staleCache ) {
# Use the stale cache while some other thread constructs the new one
@@ -385,22 +323,19 @@ class MessageCache {
$success = true;
break;
} elseif ( $failedAttempts > 0 ) {
- # Already retried once, still failed, so don't do another lock/unlock cycle
+ # Already blocked once, so avoid another lock/unlock cycle.
# This case will typically be hit if memcached is down, or if
- # loadFromDB() takes longer than MSG_WAIT_TIMEOUT
+ # loadFromDB() takes longer than LOCK_WAIT.
$where[] = "could not acquire status key.";
break;
+ } elseif ( $loadStatus === 'cantacquire' ) {
+ # Wait for the other thread to finish, then retry. Normally,
+ # the memcached get() will then yeild the other thread's result.
+ $where[] = 'waited for other thread to complete';
+ $this->getReentrantScopedLock( $cacheKey );
} else {
- $status = $this->mMemc->get( $statusKey );
- if ( $status === 'error' ) {
- # Disable cache
- break;
- } else {
- # Wait for the other thread to finish, then retry
- $where[] = 'waited for other thread to complete';
- $this->lock( $cacheKey );
- $this->unlock( $cacheKey );
- }
+ # Disable cache; $loadStatus is 'disabled'
+ break;
}
}
}
@@ -415,6 +350,7 @@ class MessageCache {
# All good, just record the success
$this->mLoadedLanguages[$code] = true;
}
+
$info = implode( ', ', $where );
wfDebugLog( 'MessageCache', __METHOD__ . ": Loading $code... $info\n" );
@@ -422,16 +358,82 @@ class MessageCache {
}
/**
+ * @param string $code
+ * @param array $where List of wfDebug() comments
+ * @param integer $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
+ * @return bool|string True on success or one of ("cantacquire", "disabled")
+ */
+ protected function loadFromDBWithLock( $code, array &$where, $mode = null ) {
+ global $wgUseLocalMessageCache;
+
+ # If cache updates on all levels fail, give up on message overrides.
+ # This is to avoid easy site outages; see $saveSuccess comments below.
+ $statusKey = wfMemcKey( 'messages', $code, 'status' );
+ $status = $this->mMemc->get( $statusKey );
+ if ( $status === 'error' ) {
+ $where[] = "could not load; method is still globally disabled";
+ return 'disabled';
+ }
+
+ # Now let's regenerate
+ $where[] = 'loading from database';
+
+ # Lock the cache to prevent conflicting writes.
+ # This lock is non-blocking so stale cache can quickly be used.
+ # Note that load() will call a blocking getReentrantScopedLock()
+ # after this if it really need to wait for any current thread.
+ $cacheKey = wfMemcKey( 'messages', $code );
+ $scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 );
+ if ( !$scopedLock ) {
+ $where[] = 'could not acquire main lock';
+ return 'cantacquire';
+ }
+
+ $cache = $this->loadFromDB( $code, $mode );
+ $this->mCache[$code] = $cache;
+ $saveSuccess = $this->saveToCaches( $cache, 'all', $code );
+
+ if ( !$saveSuccess ) {
+ # Cache save has failed.
+ # There are two main scenarios where this could be a problem:
+ #
+ # - The cache is more than the maximum size (typically
+ # 1MB compressed).
+ #
+ # - Memcached has no space remaining in the relevant slab
+ # class. This is unlikely with recent versions of
+ # memcached.
+ #
+ # Either way, if there is a local cache, nothing bad will
+ # happen. If there is no local cache, disabling the message
+ # cache for all requests avoids incurring a loadFromDB()
+ # overhead on every request, and thus saves the wiki from
+ # complete downtime under moderate traffic conditions.
+ if ( !$wgUseLocalMessageCache ) {
+ $this->mMemc->set( $statusKey, 'error', 60 * 5 );
+ $where[] = 'could not save cache, disabled globally for 5 minutes';
+ } else {
+ $where[] = "could not save global cache";
+ }
+ }
+
+ return true;
+ }
+
+ /**
* Loads cacheable messages from the database. Messages bigger than
* $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
* on-demand from the database later.
*
- * @param string $code Language code.
- * @return array Loaded messages for storing in caches.
+ * @param string $code Language code
+ * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
+ * @return array Loaded messages for storing in caches
*/
- function loadFromDB( $code ) {
+ function loadFromDB( $code, $mode = null ) {
global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
- $dbr = wfGetDB( DB_SLAVE );
+
+ $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE );
+
$cache = array();
# Common conditions
@@ -502,6 +504,8 @@ class MessageCache {
}
$cache['VERSION'] = MSG_CACHE_VERSION;
+ ksort( $cache );
+ $cache['HASH'] = md5( serialize( $cache ) );
$cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
return $cache;
@@ -510,41 +514,57 @@ class MessageCache {
/**
* Updates cache as necessary when message page is changed
*
- * @param string $title Name of the page changed.
+ * @param string|bool $title Name of the page changed (false if deleted)
* @param mixed $text New contents of the page.
*/
public function replace( $title, $text ) {
- global $wgMaxMsgCacheEntrySize;
+ global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode;
if ( $this->mDisable ) {
-
return;
}
list( $msg, $code ) = $this->figureMessage( $title );
+ if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) {
+ // Content language overrides do not use the /<code> suffix
+ return;
+ }
+ // Note that if the cache is volatile, load() may trigger a DB fetch.
+ // In that case we reenter/reuse the existing cache key lock to avoid
+ // a self-deadlock. This is safe as no reads happen *directly* in this
+ // method between getReentrantScopedLock() and load() below. There is
+ // no risk of data "changing under our feet" for replace().
$cacheKey = wfMemcKey( 'messages', $code );
- $this->load( $code );
- $this->lock( $cacheKey );
+ $scopedLock = $this->getReentrantScopedLock( $cacheKey );
+ $this->load( $code, self::FOR_UPDATE );
$titleKey = wfMemcKey( 'messages', 'individual', $title );
-
if ( $text === false ) {
- # Article was deleted
+ // Article was deleted
$this->mCache[$code][$title] = '!NONEXISTENT';
- $this->mMemc->delete( $titleKey );
+ $this->wanCache->delete( $titleKey );
} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- # Check for size
+ // Check for size
$this->mCache[$code][$title] = '!TOO BIG';
- $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry );
+ $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
} else {
$this->mCache[$code][$title] = ' ' . $text;
- $this->mMemc->delete( $titleKey );
+ $this->wanCache->delete( $titleKey );
}
- # Update caches
- $this->saveToCaches( $this->mCache[$code], 'all', $code );
- $this->unlock( $cacheKey );
+ // Mark this cache as definitely "latest" (non-volatile) so
+ // load() calls do try to refresh the cache with slave data
+ $this->mCache[$code]['LATEST'] = time();
+
+ // Update caches if the lock was acquired
+ if ( $scopedLock ) {
+ $this->saveToCaches( $this->mCache[$code], 'all', $code );
+ }
+
+ ScopedCallback::consume( $scopedLock );
+ // Relay the purge to APC and other DCs
+ $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
// Also delete cached sidebar... just in case it is affected
$codes = array( $code );
@@ -554,19 +574,16 @@ class MessageCache {
$codes = array_keys( Language::fetchLanguageNames() );
}
- global $wgMemc;
foreach ( $codes as $code ) {
$sidebarKey = wfMemcKey( 'sidebar', $code );
- $wgMemc->delete( $sidebarKey );
+ $this->wanCache->delete( $sidebarKey, 5 );
}
// Update the message in the message blob store
- global $wgContLang;
$blobStore = new MessageBlobStore();
$blobStore->updateMessage( $wgContLang->lcfirst( $msg ) );
Hooks::run( 'MessageCacheReplace', array( $title, $text ) );
-
}
/**
@@ -598,63 +615,79 @@ class MessageCache {
* @param string|bool $code Language code (default: false)
* @return bool
*/
- protected function saveToCaches( $cache, $dest, $code = false ) {
- global $wgUseLocalMessageCache;
-
- $cacheKey = wfMemcKey( 'messages', $code );
-
+ protected function saveToCaches( array $cache, $dest, $code = false ) {
if ( $dest === 'all' ) {
+ $cacheKey = wfMemcKey( 'messages', $code );
$success = $this->mMemc->set( $cacheKey, $cache );
} else {
$success = true;
}
- # Save to local cache
- if ( $wgUseLocalMessageCache ) {
- $serialized = serialize( $cache );
- $hash = md5( $serialized );
- $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash );
- $this->saveToLocal( $serialized, $hash, $code );
- }
+ $this->setValidationHash( $code, $cache );
+ $this->saveToLocalCache( $code, $cache );
return $success;
}
/**
- * Represents a write lock on the messages key.
+ * Get the md5 used to validate the local APC cache
*
- * Will retry MessageCache::MSG_WAIT_TIMEOUT times, each operations having
- * a timeout of MessageCache::MSG_LOCK_TIMEOUT.
- *
- * @param string $key
- * @return bool Success
+ * @param string $code
+ * @return array (hash or false, bool expiry/volatility status)
*/
- function lock( $key ) {
- $lockKey = $key . ':lock';
- $acquired = false;
- $testDone = false;
- for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) {
- $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT );
- if ( $acquired ) {
- break;
- }
+ protected function getValidationHash( $code ) {
+ $curTTL = null;
+ $value = $this->wanCache->get(
+ wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ $curTTL,
+ array( wfMemcKey( 'messages', $code ) )
+ );
- # Fail fast if memcached is totally down
- if ( !$testDone ) {
- $testDone = true;
- if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) {
- break;
- }
+ if ( !$value ) {
+ // No hash found at all; cache must regenerate to be safe
+ $hash = false;
+ $expired = true;
+ } else {
+ $hash = $value['hash'];
+ if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
+ // Cache was recently updated via replace() and should be up-to-date
+ $expired = false;
+ } else {
+ // See if the "check" key was bumped after the hash was generated
+ $expired = ( $curTTL < 0 );
}
- sleep( 1 );
}
- return $acquired;
+ return array( $hash, $expired );
}
- function unlock( $key ) {
- $lockKey = $key . ':lock';
- $this->mMemc->delete( $lockKey );
+ /**
+ * Set the md5 used to validate the local disk cache
+ *
+ * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
+ * be treated as "volatile" by getValidationHash() for the next few seconds
+ *
+ * @param string $code
+ * @param array $cache Cached messages with a version
+ */
+ protected function setValidationHash( $code, array $cache ) {
+ $this->wanCache->set(
+ wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ array(
+ 'hash' => $cache['HASH'],
+ 'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
+ ),
+ WANObjectCache::TTL_NONE
+ );
+ }
+
+ /**
+ * @param string $key A language message cache key that stores blobs
+ * @param integer $timeout Wait timeout in seconds
+ * @return null|ScopedCallback
+ */
+ protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) {
+ return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ );
}
/**
@@ -713,12 +746,7 @@ class MessageCache {
}
// Normalise title-case input (with some inlining)
- $lckey = strtr( $key, ' ', '_' );
- if ( ord( $lckey ) < 128 ) {
- $lckey[0] = strtolower( $lckey[0] );
- } else {
- $lckey = $wgContLang->lcfirst( $lckey );
- }
+ $lckey = MessageCache::normalizeKey( $key );
Hooks::run( 'MessageCache::get', array( &$lckey ) );
@@ -903,7 +931,7 @@ class MessageCache {
# Try the individual message cache
$titleKey = wfMemcKey( 'messages', 'individual', $title );
- $entry = $this->mMemc->get( $titleKey );
+ $entry = $this->wanCache->get( $titleKey );
if ( $entry ) {
if ( substr( $entry, 0, 1 ) === ' ' ) {
$this->mCache[$code][$title] = $entry;
@@ -917,14 +945,12 @@ class MessageCache {
return false;
} else {
# Corrupt/obsolete entry, delete it
- $this->mMemc->delete( $titleKey );
+ $this->wanCache->delete( $titleKey );
}
}
# Try loading it from the database
- $revision = Revision::newFromTitle(
- Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST
- );
+ $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
if ( $revision ) {
$content = $revision->getContent();
if ( !$content ) {
@@ -945,13 +971,13 @@ class MessageCache {
wfDebugLog(
'MessageCache',
__METHOD__ . ": message content doesn't provide wikitext "
- . "(content model: " . $content->getContentHandler() . ")"
+ . "(content model: " . $content->getModel() . ")"
);
$message = false; // negative caching
} else {
$this->mCache[$code][$title] = ' ' . $message;
- $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
+ $this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry );
}
}
} else {
@@ -960,7 +986,7 @@ class MessageCache {
if ( $message === false ) { // negative caching
$this->mCache[$code][$title] = '!NONEXISTENT';
- $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
+ $this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
}
return $message;
@@ -1042,7 +1068,8 @@ class MessageCache {
if ( !$title || !$title instanceof Title ) {
global $wgTitle;
- wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.' );
+ wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
+ wfGetAllCallers( 5 ) . ' with no title set.' );
$title = $wgTitle;
}
// Sometimes $wgTitle isn't set either...
@@ -1073,11 +1100,10 @@ class MessageCache {
function clear() {
$langs = Language::fetchLanguageNames( null, 'mw' );
foreach ( array_keys( $langs ) as $code ) {
- # Global cache
- $this->mMemc->delete( wfMemcKey( 'messages', $code ) );
- # Invalidate all local caches
- $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) );
+ # Global and local caches
+ $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
}
+
$this->mLoadedLanguages = array();
}
@@ -1087,6 +1113,7 @@ class MessageCache {
*/
public function figureMessage( $key ) {
global $wgLanguageCode;
+
$pieces = explode( '/', $key );
if ( count( $pieces ) < 2 ) {
return array( $key, $wgLanguageCode );
diff --git a/includes/cache/ResourceFileCache.php b/includes/cache/ResourceFileCache.php
index 6d26a2d5..e1186efd 100644
--- a/includes/cache/ResourceFileCache.php
+++ b/includes/cache/ResourceFileCache.php
@@ -1,6 +1,6 @@
<?php
/**
- * Resource loader request result caching in the file system.
+ * ResourceLoader request result caching in the file system.
*
* 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
@@ -22,7 +22,7 @@
*/
/**
- * Resource loader request result caching in the file system.
+ * ResourceLoader request result caching in the file system.
*
* @ingroup Cache
*/
diff --git a/includes/cache/UserCache.php b/includes/cache/UserCache.php
index 8a42489c..51bf385b 100644
--- a/includes/cache/UserCache.php
+++ b/includes/cache/UserCache.php
@@ -123,11 +123,11 @@ class UserCache {
$lb = new LinkBatch();
foreach ( $usersToCheck as $userId => $name ) {
if ( $this->queryNeeded( $userId, 'userpage', $options ) ) {
- $lb->add( NS_USER, str_replace( ' ', '_', $row->user_name ) );
+ $lb->add( NS_USER, $name );
$this->typesCached[$userId]['userpage'] = 1;
}
if ( $this->queryNeeded( $userId, 'usertalk', $options ) ) {
- $lb->add( NS_USER_TALK, str_replace( ' ', '_', $row->user_name ) );
+ $lb->add( NS_USER_TALK, $name );
$this->typesCached[$userId]['usertalk'] = 1;
}
}