MediaWiki  master
MapCacheLRU.php
Go to the documentation of this file.
1 <?php
22 
34 class MapCacheLRU implements ExpirationAwareness {
36  private $cache = [];
38  private $timestamps = [];
40  private $epoch;
41 
43  private $maxCacheKeys;
44 
46  private $wallClockOverride;
47 
49  private const RANK_TOP = 1.0;
50 
52  private const SIMPLE = 0;
54  private const FIELDS = 1;
55 
59  public function __construct( int $maxKeys ) {
60  if ( $maxKeys <= 0 ) {
61  throw new InvalidArgumentException( '$maxKeys must be above zero' );
62  }
63 
64  $this->maxCacheKeys = $maxKeys;
65  // Use the current time as the default "as of" timestamp of entries
66  $this->epoch = $this->getCurrentTime();
67  }
68 
75  public static function newFromArray( array $values, $maxKeys ) {
76  $mapCache = new self( $maxKeys );
77  $mapCache->cache = ( count( $values ) > $maxKeys )
78  ? array_slice( $values, -$maxKeys, null, true )
79  : $values;
80 
81  return $mapCache;
82  }
83 
88  public function toArray() {
89  return $this->cache;
90  }
91 
107  public function set( $key, $value, $rank = self::RANK_TOP ) {
108  if ( $this->has( $key ) ) {
109  $this->ping( $key );
110  } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
111  $evictKey = array_key_first( $this->cache );
112  unset( $this->cache[$evictKey] );
113  unset( $this->timestamps[$evictKey] );
114  }
115 
116  if ( $rank < 1.0 && $rank > 0 ) {
117  $offset = intval( $rank * count( $this->cache ) );
118  $this->cache = array_slice( $this->cache, 0, $offset, true )
119  + [ $key => $value ]
120  + array_slice( $this->cache, $offset, null, true );
121  } else {
122  $this->cache[$key] = $value;
123  }
124 
125  $this->timestamps[$key] = [
126  self::SIMPLE => $this->getCurrentTime(),
127  self::FIELDS => []
128  ];
129  }
130 
139  public function has( $key, $maxAge = INF ) {
140  if ( !is_int( $key ) && !is_string( $key ) ) {
141  throw new UnexpectedValueException(
142  __METHOD__ . ': invalid key; must be string or integer.' );
143  }
144  return array_key_exists( $key, $this->cache )
145  && (
146  // Optimization: Avoid expensive getAge/getCurrentTime for common case (T275673)
147  $maxAge === INF
148  || $maxAge <= 0
149  || $this->getKeyAge( $key ) <= $maxAge
150  );
151  }
152 
169  public function get( $key, $maxAge = INF, $default = null ) {
170  if ( !$this->has( $key, $maxAge ) ) {
171  return $default;
172  }
173 
174  $this->ping( $key );
175 
176  return $this->cache[$key];
177  }
178 
185  public function setField( $key, $field, $value, $initRank = self::RANK_TOP ) {
186  if ( $this->has( $key ) ) {
187  $this->ping( $key );
188 
189  if ( !is_array( $this->cache[$key] ) ) {
190  $type = gettype( $this->cache[$key] );
191  throw new UnexpectedValueException( "Cannot add field to non-array value ('$key' is $type)" );
192  }
193  } else {
194  $this->set( $key, [], $initRank );
195  }
196 
197  if ( !is_string( $field ) && !is_int( $field ) ) {
198  throw new UnexpectedValueException(
199  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
200  }
201 
202  $this->cache[$key][$field] = $value;
203  $this->timestamps[$key][self::FIELDS][$field] = $this->getCurrentTime();
204  }
205 
213  public function hasField( $key, $field, $maxAge = INF ) {
214  $value = $this->get( $key );
215 
216  if ( !is_int( $field ) && !is_string( $field ) ) {
217  throw new UnexpectedValueException(
218  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
219  }
220  return is_array( $value )
221  && array_key_exists( $field, $value )
222  && (
223  $maxAge === INF
224  || $maxAge <= 0
225  || $this->getKeyFieldAge( $key, $field ) <= $maxAge
226  );
227  }
228 
236  public function getField( $key, $field, $maxAge = INF ) {
237  if ( !$this->hasField( $key, $field, $maxAge ) ) {
238  return null;
239  }
240 
241  return $this->cache[$key][$field];
242  }
243 
248  public function getAllKeys() {
249  return array_keys( $this->cache );
250  }
251 
265  public function getWithSetCallback(
266  $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
267  ) {
268  if ( $this->has( $key, $maxAge ) ) {
269  $value = $this->get( $key );
270  } else {
271  $value = $callback();
272  if ( $value !== false ) {
273  $this->set( $key, $value, $rank );
274  }
275  }
276 
277  return $value;
278  }
279 
287  public function makeKey( ...$components ) {
288  // Based on BagOStuff::makeKeyInternal, except without a required
289  // $keygroup prefix. While MapCacheLRU can and is used as cache for
290  // multiple groups of keys, it is equally common for the instance itself
291  // to represent a single group, and thus have keys where the first component
292  // can directly be a user-controlled variable.
293  $key = '';
294  foreach ( $components as $i => $component ) {
295  if ( $i > 0 ) {
296  $key .= ':';
297  }
298  $key .= strtr( $component, [ '%' => '%25', ':' => '%3A' ] );
299  }
300  return $key;
301  }
302 
309  public function clear( $keys = null ) {
310  if ( func_num_args() == 0 ) {
311  $this->cache = [];
312  $this->timestamps = [];
313  } else {
314  foreach ( (array)$keys as $key ) {
315  unset( $this->cache[$key] );
316  unset( $this->timestamps[$key] );
317  }
318  }
319  }
320 
327  public function getMaxSize() {
328  return $this->maxCacheKeys;
329  }
330 
338  public function setMaxSize( int $maxKeys ) {
339  if ( $maxKeys <= 0 ) {
340  throw new InvalidArgumentException( '$maxKeys must be above zero' );
341  }
342 
343  $this->maxCacheKeys = $maxKeys;
344  while ( count( $this->cache ) > $this->maxCacheKeys ) {
345  $evictKey = array_key_first( $this->cache );
346  unset( $this->cache[$evictKey] );
347  unset( $this->timestamps[$evictKey] );
348  }
349  }
350 
356  private function ping( $key ) {
357  $item = $this->cache[$key];
358  unset( $this->cache[$key] );
359  $this->cache[$key] = $item;
360  }
361 
366  private function getKeyAge( $key ) {
367  $mtime = $this->timestamps[$key][self::SIMPLE] ?? $this->epoch;
368 
369  return ( $this->getCurrentTime() - $mtime );
370  }
371 
377  private function getKeyFieldAge( $key, $field ) {
378  $mtime = $this->timestamps[$key][self::FIELDS][$field] ?? $this->epoch;
379 
380  return ( $this->getCurrentTime() - $mtime );
381  }
382 
383  public function __serialize() {
384  return [
385  'entries' => $this->cache,
386  'timestamps' => $this->timestamps,
387  'maxCacheKeys' => $this->maxCacheKeys,
388  ];
389  }
390 
391  public function __unserialize( $data ) {
392  $this->cache = $data['entries'] ?? [];
393  $this->timestamps = $data['timestamps'] ?? [];
394  // Fallback needed for serializations prior to T218511
395  $this->maxCacheKeys = $data['maxCacheKeys'] ?? ( count( $this->cache ) + 1 );
396  $this->epoch = $this->getCurrentTime();
397  }
398 
403  protected function getCurrentTime() {
404  return $this->wallClockOverride ?: microtime( true );
405  }
406 
411  public function setMockTime( &$time ) {
412  $this->wallClockOverride =& $time;
413  }
414 }
Store key-value entries in a size-limited in-memory LRU cache.
Definition: MapCacheLRU.php:34
setMaxSize(int $maxKeys)
Resize the maximum number of cache entries, removing older entries as needed.
has( $key, $maxAge=INF)
Check if a key exists.
__unserialize( $data)
static newFromArray(array $values, $maxKeys)
Definition: MapCacheLRU.php:75
getField( $key, $field, $maxAge=INF)
setMockTime(&$time)
hasField( $key, $field, $maxAge=INF)
clear( $keys=null)
Clear one or several cache entries, or all cache entries.
getMaxSize()
Get the maximum number of keys allowed.
setField( $key, $field, $value, $initRank=self::RANK_TOP)
makeKey(... $components)
Format a cache key string.
__construct(int $maxKeys)
Definition: MapCacheLRU.php:59
getWithSetCallback( $key, callable $callback, $rank=self::RANK_TOP, $maxAge=INF)
Get an item with the given key, producing and setting it if not found.
Generic interface providing Time-To-Live constants for expirable object storage.