MediaWiki  master
MapCacheLRU.php
Go to the documentation of this file.
1 <?php
24 
36 class MapCacheLRU implements ExpirationAwareness {
38  private $cache = [];
40  private $timestamps = [];
42  private $epoch;
43 
45  private $maxCacheKeys;
46 
48  private $wallClockOverride;
49 
51  private const RANK_TOP = 1.0;
52 
54  private const SIMPLE = 0;
56  private const FIELDS = 1;
57 
61  public function __construct( int $maxKeys ) {
62  if ( $maxKeys <= 0 ) {
63  throw new InvalidArgumentException( '$maxKeys must be above zero' );
64  }
65 
66  $this->maxCacheKeys = $maxKeys;
67  // Use the current time as the default "as of" timestamp of entries
68  $this->epoch = $this->getCurrentTime();
69  }
70 
77  public static function newFromArray( array $values, $maxKeys ) {
78  $mapCache = new self( $maxKeys );
79  $mapCache->cache = ( count( $values ) > $maxKeys )
80  ? array_slice( $values, -$maxKeys, null, true )
81  : $values;
82 
83  return $mapCache;
84  }
85 
90  public function toArray() {
91  return $this->cache;
92  }
93 
109  public function set( $key, $value, $rank = self::RANK_TOP ) {
110  if ( $this->has( $key ) ) {
111  $this->ping( $key );
112  } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
113  $evictKey = array_key_first( $this->cache );
114  unset( $this->cache[$evictKey] );
115  unset( $this->timestamps[$evictKey] );
116  }
117 
118  if ( $rank < 1.0 && $rank > 0 ) {
119  $offset = intval( $rank * count( $this->cache ) );
120  $this->cache = array_slice( $this->cache, 0, $offset, true )
121  + [ $key => $value ]
122  + array_slice( $this->cache, $offset, null, true );
123  } else {
124  $this->cache[$key] = $value;
125  }
126 
127  $this->timestamps[$key] = [
128  self::SIMPLE => $this->getCurrentTime(),
129  self::FIELDS => []
130  ];
131  }
132 
141  public function has( $key, $maxAge = INF ) {
142  if ( !is_int( $key ) && !is_string( $key ) ) {
143  throw new UnexpectedValueException(
144  __METHOD__ . ': invalid key; must be string or integer.' );
145  }
146  return array_key_exists( $key, $this->cache )
147  && (
148  // Optimization: Avoid expensive getAge/getCurrentTime for common case (T275673)
149  $maxAge === INF
150  || $maxAge <= 0
151  || $this->getKeyAge( $key ) <= $maxAge
152  );
153  }
154 
171  public function get( $key, $maxAge = INF, $default = null ) {
172  if ( !$this->has( $key, $maxAge ) ) {
173  return $default;
174  }
175 
176  $this->ping( $key );
177 
178  return $this->cache[$key];
179  }
180 
187  public function setField( $key, $field, $value, $initRank = self::RANK_TOP ) {
188  if ( $this->has( $key ) ) {
189  $this->ping( $key );
190 
191  if ( !is_array( $this->cache[$key] ) ) {
192  $type = gettype( $this->cache[$key] );
193  throw new UnexpectedValueException( "Cannot add field to non-array value ('$key' is $type)" );
194  }
195  } else {
196  $this->set( $key, [], $initRank );
197  }
198 
199  if ( !is_string( $field ) && !is_int( $field ) ) {
200  throw new UnexpectedValueException(
201  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
202  }
203 
204  $this->cache[$key][$field] = $value;
205  $this->timestamps[$key][self::FIELDS][$field] = $this->getCurrentTime();
206  }
207 
215  public function hasField( $key, $field, $maxAge = INF ) {
216  $value = $this->get( $key );
217 
218  if ( !is_int( $field ) && !is_string( $field ) ) {
219  throw new UnexpectedValueException(
220  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
221  }
222  return is_array( $value )
223  && array_key_exists( $field, $value )
224  && (
225  $maxAge === INF
226  || $maxAge <= 0
227  || $this->getKeyFieldAge( $key, $field ) <= $maxAge
228  );
229  }
230 
238  public function getField( $key, $field, $maxAge = INF ) {
239  if ( !$this->hasField( $key, $field, $maxAge ) ) {
240  return null;
241  }
242 
243  return $this->cache[$key][$field];
244  }
245 
250  public function getAllKeys() {
251  return array_keys( $this->cache );
252  }
253 
267  public function getWithSetCallback(
268  $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
269  ) {
270  if ( $this->has( $key, $maxAge ) ) {
271  $value = $this->get( $key );
272  } else {
273  $value = $callback();
274  if ( $value !== false ) {
275  $this->set( $key, $value, $rank );
276  }
277  }
278 
279  return $value;
280  }
281 
288  public function clear( $keys = null ) {
289  if ( func_num_args() == 0 ) {
290  $this->cache = [];
291  $this->timestamps = [];
292  } else {
293  foreach ( (array)$keys as $key ) {
294  unset( $this->cache[$key] );
295  unset( $this->timestamps[$key] );
296  }
297  }
298  }
299 
306  public function getMaxSize() {
307  return $this->maxCacheKeys;
308  }
309 
317  public function setMaxSize( int $maxKeys ) {
318  if ( $maxKeys <= 0 ) {
319  throw new InvalidArgumentException( '$maxKeys must be above zero' );
320  }
321 
322  $this->maxCacheKeys = $maxKeys;
323  while ( count( $this->cache ) > $this->maxCacheKeys ) {
324  $evictKey = array_key_first( $this->cache );
325  unset( $this->cache[$evictKey] );
326  unset( $this->timestamps[$evictKey] );
327  }
328  }
329 
335  private function ping( $key ) {
336  $item = $this->cache[$key];
337  unset( $this->cache[$key] );
338  $this->cache[$key] = $item;
339  }
340 
345  private function getKeyAge( $key ) {
346  $mtime = $this->timestamps[$key][self::SIMPLE] ?? $this->epoch;
347 
348  return ( $this->getCurrentTime() - $mtime );
349  }
350 
356  private function getKeyFieldAge( $key, $field ) {
357  $mtime = $this->timestamps[$key][self::FIELDS][$field] ?? $this->epoch;
358 
359  return ( $this->getCurrentTime() - $mtime );
360  }
361 
362  public function __serialize() {
363  return [
364  'entries' => $this->cache,
365  'timestamps' => $this->timestamps,
366  'maxCacheKeys' => $this->maxCacheKeys,
367  ];
368  }
369 
370  public function __unserialize( $data ) {
371  $this->cache = $data['entries'] ?? [];
372  $this->timestamps = $data['timestamps'] ?? [];
373  // Fallback needed for serializations prior to T218511
374  $this->maxCacheKeys = $data['maxCacheKeys'] ?? ( count( $this->cache ) + 1 );
375  $this->epoch = $this->getCurrentTime();
376  }
377 
382  protected function getCurrentTime() {
383  return $this->wallClockOverride ?: microtime( true );
384  }
385 
390  public function setMockTime( &$time ) {
391  $this->wallClockOverride =& $time;
392  }
393 }
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
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:77
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)
__construct(int $maxKeys)
Definition: MapCacheLRU.php:61
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.