MediaWiki REL1_40
MapCacheLRU.php
Go to the documentation of this file.
1<?php
24
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.
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)
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)
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.