MediaWiki  master
MessageBlobStore.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\ResourceLoader;
24 
25 use FormatJson;
27 use Psr\Log\LoggerAwareInterface;
28 use Psr\Log\LoggerInterface;
29 use Psr\Log\NullLogger;
30 use WANObjectCache;
32 
42 class MessageBlobStore implements LoggerAwareInterface {
44  private $resourceloader;
45 
47  protected $logger;
48 
50  protected $wanCache;
51 
57  public function __construct(
58  ResourceLoader $rl,
59  ?LoggerInterface $logger,
60  ?WANObjectCache $wanObjectCache
61  ) {
62  $this->resourceloader = $rl;
63  $this->logger = $logger ?: new NullLogger();
64 
65  // NOTE: when changing this assignment, make sure the code in the instantiator for
66  // LocalisationCache which calls MessageBlobStore::clearGlobalCacheEntry() uses the
67  // same cache object.
68  $this->wanCache = $wanObjectCache ?: MediaWikiServices::getInstance()
69  ->getMainWANObjectCache();
70  }
71 
76  public function setLogger( LoggerInterface $logger ) {
77  $this->logger = $logger;
78  }
79 
88  public function getBlob( Module $module, $lang ) {
89  $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
90  return $blobs[$module->getName()];
91  }
92 
101  public function getBlobs( array $modules, $lang ) {
102  // Each cache key for a message blob by module name and language code also has a generic
103  // check key without language code. This is used to invalidate any and all language subkeys
104  // that exist for a module from the updateMessage() method.
105  $checkKeys = [
106  self::makeGlobalPurgeKey( $this->wanCache )
107  ];
108  $cacheKeys = [];
109  foreach ( $modules as $name => $module ) {
110  $cacheKey = $this->makeBlobCacheKey( $name, $lang, $module );
111  $cacheKeys[$name] = $cacheKey;
112  $checkKeys[$cacheKey][] = $this->makeModulePurgeKey( $name );
113  }
114  $curTTLs = [];
115  $result = $this->wanCache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
116 
117  $blobs = [];
118  foreach ( $modules as $name => $module ) {
119  $key = $cacheKeys[$name];
120  if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
121  $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
122  } else {
123  // Use unexpired cache
124  $blobs[$name] = $result[$key];
125  }
126  }
127  return $blobs;
128  }
129 
136  private static function makeGlobalPurgeKey( WANObjectCache $cache ) {
137  return $cache->makeGlobalKey( 'resourceloader-messageblob' );
138  }
139 
146  private function makeModulePurgeKey( $name ) {
147  return $this->wanCache->makeKey( 'resourceloader-messageblob', $name );
148  }
149 
156  private function makeBlobCacheKey( $name, $lang, Module $module ) {
157  $messages = array_values( array_unique( $module->getMessages() ) );
158  sort( $messages );
159  return $this->wanCache->makeKey( 'resourceloader-messageblob',
160  $name,
161  $lang,
162  md5( json_encode( $messages ) )
163  );
164  }
165 
173  protected function recacheMessageBlob( $cacheKey, Module $module, $lang ) {
174  $blob = $this->generateMessageBlob( $module, $lang );
175  $cache = $this->wanCache;
176  $cache->set( $cacheKey, $blob,
177  // Add part of a day to TTL to avoid all modules expiring at once
178  $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
180  );
181  return $blob;
182  }
183 
190  public function updateMessage( $key ): void {
191  $moduleNames = $this->resourceloader->getModulesByMessage( $key );
192  foreach ( $moduleNames as $moduleName ) {
193  // Use the default holdoff TTL to account for database replica DB lag
194  // which can affect MessageCache.
195  $this->wanCache->touchCheckKey( $this->makeModulePurgeKey( $moduleName ) );
196  }
197  }
198 
204  public function clear() {
205  self::clearGlobalCacheEntry( $this->wanCache );
206  }
207 
215  public static function clearGlobalCacheEntry( WANObjectCache $cache ) {
216  // Disable holdoff TTL because:
217  // - LocalisationCache is populated by messages on-disk and don't have DB lag,
218  // thus there is no need for hold off. We only clear it after new localisation
219  // updates are known to be deployed to all servers.
220  // - This global check key invalidates message blobs for all modules for all wikis
221  // in cache contexts (e.g. languages, skins). Setting a hold-off on this key could
222  // cause a cache stampede since no values would be stored for several seconds.
223  $cache->touchCheckKey( self::makeGlobalPurgeKey( $cache ), $cache::HOLDOFF_TTL_NONE );
224  }
225 
232  protected function fetchMessage( $key, $lang ) {
233  $message = wfMessage( $key )->inLanguage( $lang );
234  if ( !$message->exists() ) {
235  $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
236  'messageKey' => $key,
237  'lang' => $lang,
238  ] );
239  $value = null;
240  } else {
241  $value = $message->plain();
242  }
243  return $value;
244  }
245 
253  private function generateMessageBlob( Module $module, $lang ) {
254  $messages = [];
255  foreach ( $module->getMessages() as $key ) {
256  $value = $this->fetchMessage( $key, $lang );
257  // If the message does not exist, omit it from the blob so that
258  // client-side mw.message may do its own existence handling.
259  if ( $value !== null ) {
260  $messages[$key] = $value;
261  }
262  }
263 
264  $json = FormatJson::encode( (object)$messages, false, FormatJson::UTF8_OK );
265  // @codeCoverageIgnoreStart
266  if ( $json === false ) {
267  $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
268  'module' => $module->getName(),
269  'lang' => $lang,
270  ] );
271  $json = '{}';
272  }
273  // codeCoverageIgnoreEnd
274  return $json;
275  }
276 }
277 
279 class_alias( MessageBlobStore::class, 'MessageBlobStore' );
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$modules
JSON formatter wrapper class.
Definition: FormatJson.php:26
const UTF8_OK
Skip escaping most characters above U+007F for readability and compactness.
Definition: FormatJson.php:34
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
This class generates message blobs for use by ResourceLoader.
static clearGlobalCacheEntry(WANObjectCache $cache)
Invalidate cache keys for all known modules.
getBlob(Module $module, $lang)
Get the message blob for a module.
recacheMessageBlob( $cacheKey, Module $module, $lang)
getBlobs(array $modules, $lang)
Get the message blobs for a set of modules.
updateMessage( $key)
Invalidate cache keys for modules using this message key.
__construct(ResourceLoader $rl, ?LoggerInterface $logger, ?WANObjectCache $wanObjectCache)
clear()
Invalidate cache keys for all known modules.
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: Module.php:48
getMessages()
Get the messages needed for this module.
Definition: Module.php:416
getName()
Get this module's name.
Definition: Module.php:135
ResourceLoader is a loading system for JavaScript and CSS resources.
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
Multi-datacenter aware caching interface.
makeGlobalKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
touchCheckKey( $key, $holdoff=self::HOLDOFF_TTL)
Increase the last-purge timestamp of a "check" key in all datacenters.
set( $key, $value, $ttl=self::TTL_INDEFINITE, array $opts=[])
Set the value of a key in cache.
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:2847
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang