MediaWiki  master
MapCacheLRU.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\Assert\Assert;
25 
38 class MapCacheLRU implements ExpirationAwareness, Serializable {
40  private $cache = [];
42  private $timestamps = [];
44  private $epoch;
45 
47  private $maxCacheKeys;
48 
51 
53  private const RANK_TOP = 1.0;
54 
56  private const SIMPLE = 0;
58  private const FIELDS = 1;
59 
64  public function __construct( $maxKeys ) {
65  Assert::parameterType( 'integer', $maxKeys, '$maxKeys' );
66  Assert::parameter( $maxKeys > 0, '$maxKeys', 'must be above zero' );
67 
68  $this->maxCacheKeys = $maxKeys;
69  // Use the current time as the default "as of" timestamp of entries
70  $this->epoch = $this->getCurrentTime();
71  }
72 
79  public static function newFromArray( array $values, $maxKeys ) {
80  $mapCache = new self( $maxKeys );
81  $mapCache->cache = ( count( $values ) > $maxKeys )
82  ? array_slice( $values, -$maxKeys, null, true )
83  : $values;
84 
85  return $mapCache;
86  }
87 
92  public function toArray() {
93  return $this->cache;
94  }
95 
111  public function set( $key, $value, $rank = self::RANK_TOP ) {
112  if ( $this->has( $key ) ) {
113  $this->ping( $key );
114  } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
115  reset( $this->cache );
116  $evictKey = key( $this->cache );
117  unset( $this->cache[$evictKey] );
118  unset( $this->timestamps[$evictKey] );
119  }
120 
121  if ( $rank < 1.0 && $rank > 0 ) {
122  $offset = intval( $rank * count( $this->cache ) );
123  $this->cache = array_slice( $this->cache, 0, $offset, true )
124  + [ $key => $value ]
125  + array_slice( $this->cache, $offset, null, true );
126  } else {
127  $this->cache[$key] = $value;
128  }
129 
130  $this->timestamps[$key] = [
131  self::SIMPLE => $this->getCurrentTime(),
132  self::FIELDS => []
133  ];
134  }
135 
144  public function has( $key, $maxAge = INF ) {
145  if ( !is_int( $key ) && !is_string( $key ) ) {
146  throw new UnexpectedValueException(
147  __METHOD__ . ': invalid key; must be string or integer.' );
148  }
149 
150  if ( !array_key_exists( $key, $this->cache ) ) {
151  return false;
152  }
153 
154  return ( $maxAge <= 0 || $this->getAge( $key ) <= $maxAge );
155  }
156 
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  } else {
189  $this->set( $key, [], $initRank );
190  }
191 
192  if ( !is_int( $field ) && !is_string( $field ) ) {
193  throw new UnexpectedValueException(
194  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
195  }
196 
197  if ( !is_array( $this->cache[$key] ) ) {
198  $type = gettype( $this->cache[$key] );
199 
200  throw new UnexpectedValueException( "The value of '$key' ($type) is not an array." );
201  }
202 
203  $this->cache[$key][$field] = $value;
204  $this->timestamps[$key][self::FIELDS][$field] = $this->getCurrentTime();
205  }
206 
214  public function hasField( $key, $field, $maxAge = INF ) {
215  $value = $this->get( $key );
216 
217  if ( !is_int( $field ) && !is_string( $field ) ) {
218  throw new UnexpectedValueException(
219  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
220  }
221 
222  if ( !is_array( $value ) || !array_key_exists( $field, $value ) ) {
223  return false;
224  }
225 
226  return ( $maxAge <= 0 || $this->getAge( $key, $field ) <= $maxAge );
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 = call_user_func( $callback );
272  if ( $value !== false ) {
273  $this->set( $key, $value, $rank );
274  }
275  }
276 
277  return $value;
278  }
279 
286  public function clear( $keys = null ) {
287  if ( func_num_args() == 0 ) {
288  $this->cache = [];
289  $this->timestamps = [];
290  } else {
291  foreach ( (array)$keys as $key ) {
292  unset( $this->cache[$key] );
293  unset( $this->timestamps[$key] );
294  }
295  }
296  }
297 
304  public function getMaxSize() {
305  return $this->maxCacheKeys;
306  }
307 
316  public function setMaxSize( $maxKeys ) {
317  Assert::parameterType( 'integer', $maxKeys, '$maxKeys' );
318  Assert::parameter( $maxKeys > 0, '$maxKeys', 'must be above zero' );
319 
320  $this->maxCacheKeys = $maxKeys;
321  while ( count( $this->cache ) > $this->maxCacheKeys ) {
322  reset( $this->cache );
323  $evictKey = key( $this->cache );
324  unset( $this->cache[$evictKey] );
325  unset( $this->timestamps[$evictKey] );
326  }
327  }
328 
334  private function ping( $key ) {
335  $item = $this->cache[$key];
336  unset( $this->cache[$key] );
337  $this->cache[$key] = $item;
338  }
339 
345  private function getAge( $key, $field = null ) {
346  if ( $field !== null ) {
347  $mtime = $this->timestamps[$key][self::FIELDS][$field] ?? $this->epoch;
348  } else {
349  $mtime = $this->timestamps[$key][self::SIMPLE] ?? $this->epoch;
350  }
351 
352  return ( $this->getCurrentTime() - $mtime );
353  }
354 
355  public function serialize() {
356  return serialize( [
357  'entries' => $this->cache,
358  'timestamps' => $this->timestamps,
359  'maxCacheKeys' => $this->maxCacheKeys,
360  ] );
361  }
362 
363  public function unserialize( $serialized ) {
364  $data = unserialize( $serialized );
365  $this->cache = $data['entries'] ?? [];
366  $this->timestamps = $data['timestamps'] ?? [];
367  // Fallback needed for serializations prior to T218511
368  $this->maxCacheKeys = $data['maxCacheKeys'] ?? ( count( $this->cache ) + 1 );
369  $this->epoch = $this->getCurrentTime();
370  }
371 
376  protected function getCurrentTime() {
377  return $this->wallClockOverride ?: microtime( true );
378  }
379 
384  public function setMockTime( &$time ) {
385  $this->wallClockOverride =& $time;
386  }
387 }
MapCacheLRU\serialize
serialize()
Definition: MapCacheLRU.php:355
MapCacheLRU\$epoch
float $epoch
Default entry timestamp if not specified.
Definition: MapCacheLRU.php:44
MapCacheLRU\ping
ping( $key)
Push an entry to the top of the cache.
Definition: MapCacheLRU.php:334
MapCacheLRU\getField
getField( $key, $field, $maxAge=INF)
Definition: MapCacheLRU.php:236
Wikimedia\LightweightObjectStore\ExpirationAwareness
Generic interface providing Time-To-Live constants for expirable object storage.
Definition: ExpirationAwareness.php:32
$serialized
foreach( $res as $row) $serialized
Definition: testCompression.php:88
MapCacheLRU\getAllKeys
getAllKeys()
Definition: MapCacheLRU.php:248
MapCacheLRU\has
has( $key, $maxAge=INF)
Check if a key exists.
Definition: MapCacheLRU.php:144
MapCacheLRU\unserialize
unserialize( $serialized)
Definition: MapCacheLRU.php:363
MapCacheLRU\$cache
array $cache
Map of (key => value)
Definition: MapCacheLRU.php:40
MapCacheLRU\getAge
getAge( $key, $field=null)
Definition: MapCacheLRU.php:345
MapCacheLRU\__construct
__construct( $maxKeys)
Definition: MapCacheLRU.php:64
MapCacheLRU\hasField
hasField( $key, $field, $maxAge=INF)
Definition: MapCacheLRU.php:214
MapCacheLRU\newFromArray
static newFromArray(array $values, $maxKeys)
Definition: MapCacheLRU.php:79
MapCacheLRU\$maxCacheKeys
int $maxCacheKeys
Max number of entries.
Definition: MapCacheLRU.php:47
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:38
MapCacheLRU\$timestamps
array $timestamps
Map of (key => (UNIX timestamp, (field => UNIX timestamp)))
Definition: MapCacheLRU.php:42
MapCacheLRU\getWithSetCallback
getWithSetCallback( $key, callable $callback, $rank=self::RANK_TOP, $maxAge=INF)
Get an item with the given key, producing and setting it if not found.
Definition: MapCacheLRU.php:265
MapCacheLRU\getMaxSize
getMaxSize()
Get the maximum number of keys allowed.
Definition: MapCacheLRU.php:304
MapCacheLRU\$wallClockOverride
float null $wallClockOverride
Definition: MapCacheLRU.php:50
MapCacheLRU\setField
setField( $key, $field, $value, $initRank=self::RANK_TOP)
Definition: MapCacheLRU.php:185
MapCacheLRU\setMaxSize
setMaxSize( $maxKeys)
Resize the maximum number of cache entries, removing older entries as needed.
Definition: MapCacheLRU.php:316
$keys
$keys
Definition: testCompression.php:72
MapCacheLRU\clear
clear( $keys=null)
Clear one or several cache entries, or all cache entries.
Definition: MapCacheLRU.php:286
MapCacheLRU\setMockTime
setMockTime(&$time)
Definition: MapCacheLRU.php:384
MapCacheLRU\toArray
toArray()
Definition: MapCacheLRU.php:92
MapCacheLRU\getCurrentTime
getCurrentTime()
Definition: MapCacheLRU.php:376
$type
$type
Definition: testCompression.php:52