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 
40 }
41 
51 class MessageBlobStore implements LoggerAwareInterface {
53  private $resourceloader;
54 
56  protected $logger;
57 
59  protected $wanCache;
60 
66  public function __construct(
67  ResourceLoader $rl,
68  ?LoggerInterface $logger,
69  ?WANObjectCache $wanObjectCache
70  ) {
71  $this->resourceloader = $rl;
72  $this->logger = $logger ?: new NullLogger();
73 
74  // NOTE: when changing this assignment, make sure the code in the instantiator for
75  // LocalisationCache which calls MessageBlobStore::clearGlobalCacheEntry() uses the
76  // same cache object.
77  $this->wanCache = $wanObjectCache ?: MediaWikiServices::getInstance()
78  ->getMainWANObjectCache();
79  }
80 
85  public function setLogger( LoggerInterface $logger ) {
86  $this->logger = $logger;
87  }
88 
97  public function getBlob( Module $module, $lang ) {
98  $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
99  return $blobs[$module->getName()];
100  }
101 
110  public function getBlobs( array $modules, $lang ) {
111  // Each cache key for a message blob by module name and language code also has a generic
112  // check key without language code. This is used to invalidate any and all language subkeys
113  // that exist for a module from the updateMessage() method.
115  $checkKeys = [
116  // Global check key, see clear()
117  $cache->makeGlobalKey( __CLASS__ )
118  ];
119  $cacheKeys = [];
120  foreach ( $modules as $name => $module ) {
121  $cacheKey = $this->makeCacheKey( $module, $lang );
122  $cacheKeys[$name] = $cacheKey;
123  // Per-module check key, see updateMessage()
124  $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
125  }
126  $curTTLs = [];
127  $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
128 
129  $blobs = [];
130  foreach ( $modules as $name => $module ) {
131  $key = $cacheKeys[$name];
132  if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
133  $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
134  } else {
135  // Use unexpired cache
136  $blobs[$name] = $result[$key];
137  }
138  }
139  return $blobs;
140  }
141 
148  private function makeCacheKey( Module $module, $lang ) {
149  $messages = array_values( array_unique( $module->getMessages() ) );
150  sort( $messages );
151  return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
152  md5( json_encode( $messages ) )
153  );
154  }
155 
163  protected function recacheMessageBlob( $cacheKey, Module $module, $lang ) {
164  $blob = $this->generateMessageBlob( $module, $lang );
166  $cache->set( $cacheKey, $blob,
167  // Add part of a day to TTL to avoid all modules expiring at once
168  $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
170  );
171  return $blob;
172  }
173 
180  public function updateMessage( $key ): void {
181  $moduleNames = $this->resourceloader->getModulesByMessage( $key );
182  foreach ( $moduleNames as $moduleName ) {
183  // Uses a holdoff to account for database replica DB lag (for MessageCache)
184  $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
185  }
186  }
187 
191  public function clear() {
192  self::clearGlobalCacheEntry( $this->wanCache );
193  }
194 
202  public static function clearGlobalCacheEntry( WANObjectCache $cache ) {
203  // Disable hold-off because:
204  // - LocalisationCache is populated by messages on-disk and don't have DB lag,
205  // thus there is no need for hold off. We only clear it after new localisation
206  // updates are known to be deployed to all servers.
207  // - This global check key invalidates message blobs for all modules for all wikis
208  // in cache contexts (e.g. languages, skins). Setting a hold-off on this key could
209  // cause a cache stampede since no values would be stored for several seconds.
210  $cache->touchCheckKey( $cache->makeGlobalKey( __CLASS__ ), $cache::HOLDOFF_TTL_NONE );
211  }
212 
219  protected function fetchMessage( $key, $lang ) {
220  $message = wfMessage( $key )->inLanguage( $lang );
221  if ( !$message->exists() ) {
222  $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
223  'messageKey' => $key,
224  'lang' => $lang,
225  ] );
226  $value = null;
227  } else {
228  $value = $message->plain();
229  }
230  return $value;
231  }
232 
240  private function generateMessageBlob( Module $module, $lang ) {
241  $messages = [];
242  foreach ( $module->getMessages() as $key ) {
243  $value = $this->fetchMessage( $key, $lang );
244  // If the message does not exist, omit it from the blob so that
245  // client-side mw.message may do its own existence handling.
246  if ( $value !== null ) {
247  $messages[$key] = $value;
248  }
249  }
250 
251  $json = FormatJson::encode( (object)$messages, false, FormatJson::UTF8_OK );
252  // @codeCoverageIgnoreStart
253  if ( $json === false ) {
254  $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
255  'module' => $module->getName(),
256  'lang' => $lang,
257  ] );
258  $json = '{}';
259  }
260  // codeCoverageIgnoreEnd
261  return $json;
262  }
263 }
264 
266 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.
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.
generateMessageBlob(Module $module, $lang)
Generate the message blob for a given module in a given language.
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:417
getName()
Get this module's name.
Definition: Module.php:135
PHP 7.2 hack to work around the issue described at https://phabricator.wikimedia.org/T166010#5962098 ...
ResourceLoader is a loading system for JavaScript and CSS resources.
Module[] $modules
Map of (module name => ResourceLoaderModule)
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
Multi-datacenter aware caching interface.
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4036
$cache
Definition: mcc.php:33
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang