libs/objectcache: Minor code cleanup
[mediawiki.git] / includes / libs / objectcache / BagOStuff.php
blob334e907db95c389672a9dc49623b64918c037400
1 <?php
2 /**
3 * Copyright © 2003-2004 Brion Vibber <brion@pobox.com>
4 * https://www.mediawiki.org/
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
21 * @file
22 * @ingroup Cache
25 /**
26 * @defgroup Cache Cache
29 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
30 use Psr\Log\LoggerAwareInterface;
31 use Psr\Log\LoggerInterface;
32 use Psr\Log\NullLogger;
33 use Wikimedia\LightweightObjectStore\ExpirationAwareness;
34 use Wikimedia\LightweightObjectStore\StorageAwareness;
35 use Wikimedia\ScopedCallback;
37 /**
38 * Class representing a cache/ephemeral data store
40 * This interface is intended to be more or less compatible with the PHP memcached client.
42 * Class instances should be created with an intended access scope for the dataset, such as:
43 * - a) A single PHP thread on a server (e.g. stored in a PHP variable)
44 * - b) A single application server (e.g. stored in APC or sqlite)
45 * - c) All application servers in datacenter (e.g. stored in memcached or mysql)
46 * - d) All application servers in all datacenters (e.g. stored via mcrouter or dynomite)
48 * Callers should use the proper factory methods that yield BagOStuff instances. Site admins
49 * should make sure that the configuration for those factory methods match their access scope.
50 * BagOStuff subclasses have widely varying levels of support replication features within and
51 * among datacenters.
53 * Subclasses should override the default "segmentationSize" field with an appropriate value.
54 * The value should not be larger than what the backing store (by default) supports. It also
55 * should be roughly informed by common performance bottlenecks (e.g. values over a certain size
56 * having poor scalability). The same goes for the "segmentedValueMaxSize" member, which limits
57 * the maximum size and chunk count (indirectly) of values.
59 * A few notes about data consistency for BagOStuff instances:
60 * - Read operation methods, e.g. get(), should be synchronous in the local datacenter.
61 * When used with READ_LATEST, such operations should reflect any prior writes originating
62 * from the local datacenter (e.g. by avoiding replica DBs or invoking quorom reads).
63 * - Write operation methods, e.g. set(), should be synchronous in the local datacenter, with
64 * asynchronous cross-datacenter replication. This replication can be either "best effort"
65 * or eventually consistent. When used with WRITE_SYNC, such operations will wait until all
66 * datacenters are updated or a timeout occurs. If the write succeeded, then any subsequent
67 * get() operations with READ_LATEST, regardless of datacenter, should reflect the changes.
68 * - Locking operation methods, e.g. lock(), unlock(), and getScopedLock(), should only apply
69 * to the local datacenter.
70 * - Any set of single-key write operation method calls originating from a single datacenter
71 * should observe "best effort" linearizability. Any set of single-key write operations using
72 * WRITE_SYNC, regardless of the datacenter, should observe "best effort" linearizability.
73 * In this context, "best effort" means that consistency holds as long as connectivity is
74 * strong, network latency is low, and there are no relevant storage server failures.
75 * Per https://en.wikipedia.org/wiki/PACELC_theorem, the store should act as a PA/EL
76 * distributed system for these operations.
78 * @stable to extend
79 * @newable
80 * @ingroup Cache
82 abstract class BagOStuff implements
83 ExpirationAwareness,
84 StorageAwareness,
85 IStoreKeyEncoder,
86 LoggerAwareInterface
88 /** @var StatsdDataFactoryInterface */
89 protected $stats;
90 /** @var LoggerInterface */
91 protected $logger;
92 /** @var callable|null */
93 protected $asyncHandler;
94 /**
95 * @var array<string,array> Cache key processing callbacks and info for metrics
96 * @phan-var array<string,array{0:string,1:callable}>
98 protected $wrapperInfoByPrefix = [];
100 /** @var int[] Map of (BagOStuff:ATTR_* constant => BagOStuff:QOS_* constant) */
101 protected $attrMap = [];
103 /** @var string Default keyspace; used by makeKey() */
104 protected $keyspace;
106 /** @var int BagOStuff:ERR_* constant of the last error that occurred */
107 protected $lastError = self::ERR_NONE;
108 /** @var int Error event sequence number of the last error that occurred */
109 protected $lastErrorId = 0;
111 /** @var int Next sequence number to use for watch/error events */
112 protected static $nextErrorMonitorId = 1;
114 /** @var float|null */
115 private $wallClockOverride;
117 /** Bitfield constants for get()/getMulti(); these are only advisory */
118 // if supported, avoid reading stale data due to replication
119 public const READ_LATEST = 1;
120 // promise that the caller handles detection of staleness
121 public const READ_VERIFIED = 2;
123 /** Bitfield constants for set()/merge(); these are only advisory */
124 // if supported, block until the write is fully replicated
125 public const WRITE_SYNC = 4;
126 // only change state of the in-memory cache
127 public const WRITE_CACHE_ONLY = 8;
128 // allow partitioning of the value if it is large
129 public const WRITE_ALLOW_SEGMENTS = 16;
130 // delete all the segments if the value is partitioned
131 public const WRITE_PRUNE_SEGMENTS = 32;
132 // if supported, do not block on completion until the next read
133 public const WRITE_BACKGROUND = 64;
135 /** @var string Global keyspace; used by makeGlobalKey() */
136 protected const GLOBAL_KEYSPACE = 'global';
137 /** @var string Precomputed global cache key prefix (needs no encoding) */
138 protected const GLOBAL_PREFIX = 'global:';
140 /** @var int Item is a single cache key */
141 protected const ARG0_KEY = 0;
142 /** @var int Item is an array of cache keys */
143 protected const ARG0_KEYARR = 1;
144 /** @var int Item is an array indexed by cache keys */
145 protected const ARG0_KEYMAP = 2;
146 /** @var int Item does not involve any keys */
147 protected const ARG0_NONKEY = 3;
149 /** @var int Item is an array indexed by cache keys */
150 protected const RES_KEYMAP = 0;
151 /** @var int Item does not involve any keys */
152 protected const RES_NONKEY = 1;
154 /** Key to the metric group to use for the relevant cache wrapper */
155 private const WRAPPER_STATS_GROUP = 0;
156 /** Key to the callback that extracts collection names from cache wrapper keys */
157 private const WRAPPER_COLLECTION_CALLBACK = 1;
160 * @stable to call
161 * @param array $params Parameters include:
162 * - keyspace: Keyspace to use for keys in makeKey(). [Default: "local"]
163 * - asyncHandler: Callable to use for scheduling tasks after the web request ends.
164 * In CLI mode, it should run the task immediately. [Default: null]
165 * - stats: IStatsdDataFactory instance. [optional]
166 * - logger: Psr\Log\LoggerInterface instance. [optional]
167 * @phan-param array{keyspace?:string,logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params
169 public function __construct( array $params = [] ) {
170 $this->keyspace = $params['keyspace'] ?? 'local';
171 $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
172 $this->setLogger( $params['logger'] ?? new NullLogger() );
174 $asyncHandler = $params['asyncHandler'] ?? null;
175 if ( is_callable( $asyncHandler ) ) {
176 $this->asyncHandler = $asyncHandler;
181 * @param LoggerInterface $logger
182 * @return void
184 public function setLogger( LoggerInterface $logger ) {
185 $this->logger = $logger;
189 * @since 1.35
190 * @return LoggerInterface
192 public function getLogger(): LoggerInterface {
193 return $this->logger;
197 * Get an item with the given key, regenerating and setting it if not found
199 * The callback can take $exptime as argument by reference and modify it.
200 * Nothing is stored nor deleted if the callback returns false.
202 * @param string $key
203 * @param int $exptime Time-to-live (seconds)
204 * @param callable $callback Callback that derives the new value
205 * @param int $flags Bitfield of BagOStuff::READ_* or BagOStuff::WRITE_* constants [optional]
206 * @return mixed The cached value if found or the result of $callback otherwise
207 * @since 1.27
209 final public function getWithSetCallback( $key, $exptime, $callback, $flags = 0 ) {
210 $value = $this->get( $key, $flags );
212 if ( $value === false ) {
213 $value = $callback( $exptime );
214 if ( $value !== false && $exptime >= 0 ) {
215 $this->set( $key, $value, $exptime, $flags );
219 return $value;
223 * Get an item with the given key
225 * If the key includes a deterministic input hash (e.g. the key can only have
226 * the correct value) or complete staleness checks are handled by the caller
227 * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
228 * This lets tiered backends know they can safely upgrade a cached value to
229 * higher tiers using standard TTLs.
231 * @param string $key
232 * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
233 * @return mixed Returns false on failure or if the item does not exist
235 abstract public function get( $key, $flags = 0 );
238 * Set an item
240 * @param string $key
241 * @param mixed $value
242 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
243 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
244 * @return bool Success
246 abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
249 * Delete an item
251 * For large values written using WRITE_ALLOW_SEGMENTS, this only deletes the main
252 * segment list key unless WRITE_PRUNE_SEGMENTS is in the flags. While deleting the segment
253 * list key has the effect of functionally deleting the key, it leaves unused blobs in cache.
255 * @param string $key
256 * @return bool True if the item was deleted or not found, false on failure
257 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
259 abstract public function delete( $key, $flags = 0 );
262 * Insert an item if it does not already exist
264 * @param string $key
265 * @param mixed $value
266 * @param int $exptime
267 * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
268 * @return bool Success
270 abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
273 * Merge changes into the existing cache value (possibly creating a new one)
275 * The callback function returns the new value given the current value
276 * (which will be false if not present), and takes the arguments:
277 * (this BagOStuff, cache key, current value, TTL).
278 * The TTL parameter is reference set to $exptime. It can be overridden in the callback.
279 * Nothing is stored nor deleted if the callback returns false.
281 * @param string $key
282 * @param callable $callback Callback method to be executed
283 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
284 * @param int $attempts The amount of times to attempt a merge in case of failure
285 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
286 * @return bool Success
287 * @throws InvalidArgumentException
289 abstract public function merge(
290 $key,
291 callable $callback,
292 $exptime = 0,
293 $attempts = 10,
294 $flags = 0
298 * Change the expiration on a key if it exists
300 * If an expiry in the past is given then the key will immediately be expired
302 * For large values written using WRITE_ALLOW_SEGMENTS, this only changes the TTL of the
303 * main segment list key. While lowering the TTL of the segment list key has the effect of
304 * functionally lowering the TTL of the key, it might leave unused blobs in cache for longer.
305 * Raising the TTL of such keys is not effective, since the expiration of a single segment
306 * key effectively expires the entire value.
308 * @param string $key
309 * @param int $exptime TTL or UNIX timestamp
310 * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
311 * @return bool Success Returns false on failure or if the item does not exist
312 * @since 1.28
314 abstract public function changeTTL( $key, $exptime = 0, $flags = 0 );
317 * Acquire an advisory lock on a key string, exclusive to the caller
319 * @param string $key
320 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
321 * @param int $exptime Lock time-to-live in seconds; 1 day maximum [optional]
322 * @param string $rclass If this thread already holds the lock, and the lock was acquired
323 * using the same value for this parameter, then return true and use reference counting so
324 * that only the unlock() call from the outermost lock() caller actually releases the lock
325 * (note that only the outermost time-to-live is used) [optional]
326 * @return bool Success
328 abstract public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' );
331 * Release an advisory lock on a key string
333 * @param string $key
334 * @return bool Success
336 abstract public function unlock( $key );
339 * Get a lightweight exclusive self-unlocking lock
341 * Note that the same lock cannot be acquired twice.
343 * This is useful for task de-duplication or to avoid obtrusive
344 * (though non-corrupting) DB errors like INSERT key conflicts
345 * or deadlocks when using LOCK IN SHARE MODE.
347 * @param string $key
348 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
349 * @param int $expiry Lock expiry [optional]; 1 day maximum
350 * @param string $rclass Allow reentry if set and the current lock used this value
351 * @return ScopedCallback|null Returns null on failure
352 * @since 1.26
354 final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) {
355 $expiry = min( $expiry ?: INF, self::TTL_DAY );
357 if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) {
358 return null;
361 return new ScopedCallback( function () use ( $key, $expiry ) {
362 $this->unlock( $key );
363 } );
367 * Delete all objects expiring before a certain date
369 * @param string|int $timestamp The reference date in MW or TS_UNIX format
370 * @param callable|null $progress Optional, a function which will be called
371 * regularly during long-running operations with the percentage progress
372 * as the first parameter. [optional]
373 * @param int|float $limit Maximum number of keys to delete [default: INF]
374 * @param string|null $tag Tag to purge a single shard only.
375 * This is only supported when server tags are used in configuration.
376 * @return bool Success; false if unimplemented
378 abstract public function deleteObjectsExpiringBefore(
379 $timestamp,
380 callable $progress = null,
381 $limit = INF,
382 string $tag = null
386 * Get an associative array containing the item for each of the keys that have items
388 * @param string[] $keys List of keys
389 * @param int $flags Bitfield; supports READ_LATEST [optional]
390 * @return mixed[] Map of (key => value) for existing keys
392 abstract public function getMulti( array $keys, $flags = 0 );
395 * Batch insertion/replace
397 * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
399 * WRITE_BACKGROUND can be used for bulk insertion where the response is not vital
401 * @param mixed[] $valueByKey Map of (key => value)
402 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
403 * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
404 * @return bool Success
405 * @since 1.24
407 abstract public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 );
410 * Batch deletion
412 * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
414 * WRITE_BACKGROUND can be used for bulk deletion where the response is not vital
416 * @param string[] $keys List of keys
417 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
418 * @return bool Success
419 * @since 1.33
421 abstract public function deleteMulti( array $keys, $flags = 0 );
424 * Change the expiration of multiple keys that exist
426 * @see BagOStuff::changeTTL()
428 * @param string[] $keys List of keys
429 * @param int $exptime TTL or UNIX timestamp
430 * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
431 * @return bool Success
432 * @since 1.34
434 abstract public function changeTTLMulti( array $keys, $exptime, $flags = 0 );
437 * Increase stored value of $key by $value while preserving its TTL
439 * @param string $key Key to increase
440 * @param int $value Value to add to $key (default: 1) [optional]
441 * @param int $flags Bit field of class WRITE_* constants [optional]
442 * @return int|bool New value or false on failure
443 * @deprecated Since 1.38
445 abstract public function incr( $key, $value = 1, $flags = 0 );
448 * Decrease stored value of $key by $value while preserving its TTL
450 * @param string $key
451 * @param int $value Value to subtract from $key (default: 1) [optional]
452 * @param int $flags Bit field of class WRITE_* constants [optional]
453 * @return int|bool New value or false on failure
454 * @deprecated Since 1.38
456 abstract public function decr( $key, $value = 1, $flags = 0 );
459 * Increase the value of the given key (no TTL change) if it exists or create it otherwise
461 * This will create the key with the value $init and TTL $exptime instead if not present.
462 * Callers should make sure that both ($init - $step) and $exptime are invariants for all
463 * operations to any given key. The value of $init should be at least that of $step.
465 * @param string $key Key built via makeKey() or makeGlobalKey()
466 * @param int $exptime Time-to-live (in seconds) or a UNIX timestamp expiration
467 * @param int $step Amount to increase the key value by [default: 1]
468 * @param int|null $init Value to initialize the key to if it does not exist [default: $step]
469 * @param int $flags Bit field of class WRITE_* constants [optional]
470 * @return int|bool New value or false on failure
471 * @since 1.24
473 abstract public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 );
476 * Get a "watch point" token that can be used to get the "last error" to occur after now
478 * @return int A token that the current error event
479 * @since 1.38
481 public function watchErrors() {
482 return self::$nextErrorMonitorId++;
486 * Get the "last error" registry
488 * The method should be invoked by a caller as part of the following pattern:
489 * - The caller invokes watchErrors() to get a "since token"
490 * - The caller invokes a sequence of cache operation methods
491 * - The caller invokes getLastError() with the "since token"
493 * External callers can also invoke this method as part of the following pattern:
494 * - The caller invokes clearLastError()
495 * - The caller invokes a sequence of cache operation methods
496 * - The caller invokes getLastError()
498 * @param int $watchPoint Only consider errors from after this "watch point" [optional]
499 * @return int BagOStuff:ERR_* constant for the "last error" registry
500 * @note Parameters added in 1.38: $watchPoint
501 * @since 1.23
503 public function getLastError( $watchPoint = 0 ) {
504 return ( $this->lastErrorId > $watchPoint ) ? $this->lastError : self::ERR_NONE;
508 * Clear the "last error" registry
510 * @since 1.23
511 * @deprecated Since 1.38
513 public function clearLastError() {
514 $this->lastError = self::ERR_NONE;
518 * Set the "last error" registry due to a problem encountered during an attempted operation
520 * @param int $error BagOStuff:ERR_* constant
521 * @since 1.23
523 protected function setLastError( $error ) {
524 $this->lastError = $error;
525 $this->lastErrorId = self::$nextErrorMonitorId++;
529 * Let a callback be run to avoid wasting time on special blocking calls
531 * This is hard-deprecated and non-functional since 1.39. The callback
532 * will not be called.
534 * @param callable $workCallback
535 * @since 1.28
536 * @deprecated since 1.39
538 abstract public function addBusyCallback( callable $workCallback );
541 * Make a cache key for the given keyspace and components
543 * Long components might be converted to respective hashes due to size constraints.
544 * In extreme cases, all of them might be combined into a single hash component.
546 * @internal This method should not be used outside of BagOStuff (since 1.36)
548 * @param string $keyspace Keyspace component
549 * @param string[]|int[] $components Key components (key collection name first)
550 * @return string Keyspace-prepended list of encoded components as a colon-separated value
551 * @since 1.27
553 abstract public function makeKeyInternal( $keyspace, $components );
556 * Make a cache key for the default keyspace and given components
558 * @see IStoreKeyEncoder::makeGlobalKey()
560 * @param string $collection Key collection name component
561 * @param string|int ...$components Additional, ordered, key components for entity IDs
562 * @return string Colon-separated, keyspace-prepended, ordered list of encoded components
563 * @since 1.27
565 abstract public function makeGlobalKey( $collection, ...$components );
568 * Make a cache key for the global keyspace and given components
570 * @see IStoreKeyEncoder::makeKey()
572 * @param string $collection Key collection name component
573 * @param string|int ...$components Additional, ordered, key components for entity IDs
574 * @return string Colon-separated, keyspace-prepended, ordered list of encoded components
575 * @since 1.27
577 abstract public function makeKey( $collection, ...$components );
580 * Check whether a cache key is in the global keyspace
582 * @param string $key
583 * @return bool
584 * @since 1.35
586 public function isKeyGlobal( $key ) {
587 return str_starts_with( $key, self::GLOBAL_PREFIX );
591 * @param int $flag BagOStuff::ATTR_* constant
592 * @return int BagOStuff:QOS_* constant
593 * @since 1.28
595 public function getQoS( $flag ) {
596 return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
600 * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit)
601 * @since 1.34
603 public function getSegmentationSize() {
604 return INF;
608 * @return int|float Maximum total segmented object size in bytes (INF for no limit)
609 * @since 1.34
611 public function getSegmentedValueMaxSize() {
612 return INF;
616 * @param int $field
617 * @param int $flags
618 * @return bool
619 * @since 1.34
621 final protected function fieldHasFlags( $field, $flags ) {
622 return ( ( $field & $flags ) === $flags );
626 * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
628 * @param BagOStuff[] $bags
629 * @return int[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
631 final protected function mergeFlagMaps( array $bags ) {
632 $map = [];
633 foreach ( $bags as $bag ) {
634 foreach ( $bag->attrMap as $attr => $rank ) {
635 if ( isset( $map[$attr] ) ) {
636 $map[$attr] = min( $map[$attr], $rank );
637 } else {
638 $map[$attr] = $rank;
643 return $map;
647 * Stage a set of new key values for storage and estimate the amount of bytes needed
649 * All previously prepared values will be cleared. Each of the new prepared values will be
650 * individually cleared as they get used by write operations for that key. This is done to
651 * avoid unchecked growth in PHP memory usage.
653 * Example usage:
654 * @code
655 * $valueByKey = [ $key1 => $value1, $key2 => $value2, $key3 => $value3 ];
656 * $cache->setNewPreparedValues( $valueByKey );
657 * $cache->set( $key1, $value1, $cache::TTL_HOUR );
658 * $cache->setMulti( [ $key2 => $value2, $key3 => $value3 ], $cache::TTL_HOUR );
659 * @endcode
661 * This is only useful if the caller needs an estimate of the serialized object sizes,
662 * such as cache wrappers with adaptive write slam avoidance or store wrappers with metrics.
663 * The caller cannot know the serialization format and even if it did, it could be expensive
664 * to serialize complex values twice just to get the size information before writing them to
665 * cache. This method solves both problems by making the cache instance do the serialization
666 * and having it reuse the result when the cache write happens.
668 * @param array $valueByKey Map of (cache key => PHP variable value to serialize)
669 * @return int[]|null[] Corresponding list of size estimates (null for invalid values)
670 * @since 1.35
672 abstract public function setNewPreparedValues( array $valueByKey );
675 * Register info about a caching layer class that uses BagOStuff as a backing store
677 * Object cache wrappers are classes that implement generic caching/storage functionality,
678 * use a BagOStuff instance as the backing store, and implement IStoreKeyEncoder with the
679 * same "generic" style key encoding as BagOStuff. Such wrappers transform keys before
680 * passing them to BagOStuff methods; a wrapper-specific prefix component will be prepended
681 * along with other possible additions. Transformed keys still use the "generic" BagOStuff
682 * encoding.
684 * The provided callback takes a transformed key, having the specified prefix component,
685 * and extracts the key collection name. The callback must be able to handle
686 * keys that bear the prefix (by coincidence) but do not originate from the wrapper class.
688 * Calls to this method should be idempotent.
690 * @param string $prefixComponent Key prefix component used by the wrapper
691 * @param string $statsGroup Stats group to use for metrics from this wrapper
692 * @param callable $collectionCallback Static callback that gets the key collection name
693 * @internal For use with BagOStuff and WANObjectCache only
694 * @since 1.36
696 public function registerWrapperInfoForStats(
697 string $prefixComponent,
698 string $statsGroup,
699 callable $collectionCallback
701 $this->wrapperInfoByPrefix[$prefixComponent] = [
702 self::WRAPPER_STATS_GROUP => $statsGroup,
703 self::WRAPPER_COLLECTION_CALLBACK => $collectionCallback
708 * At a minimum, there must be a keyspace and collection name component
710 * @param string|int ...$components Key components for keyspace, collection name, and IDs
711 * @return string Keyspace-prepended list of encoded components as a colon-separated value
712 * @since 1.35
714 final protected function genericKeyFromComponents( ...$components ) {
715 if ( count( $components ) < 2 ) {
716 throw new InvalidArgumentException( "Missing keyspace or collection name" );
719 $key = '';
720 foreach ( $components as $i => $component ) {
721 if ( $i > 0 ) {
722 $key .= ':';
724 // Escape delimiter (":") and escape ("%") characters
725 $key .= strtr( $component, [ '%' => '%25', ':' => '%3A' ] );
728 return $key;
732 * Extract the components from a "generic" reversible cache key
734 * @see BagOStuff::genericKeyFromComponents()
736 * @param string $key Keyspace-prepended list of encoded components as a colon-separated value
737 * @return string[] Key components for keyspace, collection name, and IDs
738 * @since 1.35
740 final protected function componentsFromGenericKey( $key ) {
741 // Note that the order of each corresponding search/replace pair matters
742 return str_replace( [ '%3A', '%25' ], [ ':', '%' ], explode( ':', $key ) );
746 * Convert a "generic" reversible cache key into one for this cache
748 * @see BagOStuff::genericKeyFromComponents()
750 * @param string $key Keyspace-prepended list of encoded components as a colon-separated value
751 * @return string Keyspace-prepended list of encoded components as a colon-separated value
753 abstract protected function convertGenericKey( $key );
756 * Call a method on behalf of wrapper BagOStuff instance that uses "generic" keys
758 * @param string $method Name of a non-final public method that reads/changes keys
759 * @param int $arg0Sig BagOStuff::ARG0_* constant describing argument 0
760 * @param int $resSig BagOStuff::RES_* constant describing the return value
761 * @param array $genericArgs Method arguments passed to the wrapper instance
762 * @param BagOStuff $wrapper The wrapper BagOStuff instance using this result
763 * @return mixed Method result with any keys remapped to "generic" keys
765 protected function proxyCall(
766 string $method,
767 int $arg0Sig,
768 int $resSig,
769 array $genericArgs,
770 BagOStuff $wrapper
772 // Get the corresponding store-specific cache keys...
773 $storeArgs = $genericArgs;
774 switch ( $arg0Sig ) {
775 case self::ARG0_KEY:
776 $storeArgs[0] = $this->convertGenericKey( $genericArgs[0] );
777 break;
778 case self::ARG0_KEYARR:
779 foreach ( $genericArgs[0] as $i => $genericKey ) {
780 $storeArgs[0][$i] = $this->convertGenericKey( $genericKey );
782 break;
783 case self::ARG0_KEYMAP:
784 $storeArgs[0] = [];
785 foreach ( $genericArgs[0] as $genericKey => $v ) {
786 $storeArgs[0][$this->convertGenericKey( $genericKey )] = $v;
788 break;
791 // Result of invoking the method with the corresponding store-specific cache keys
792 $watchPoint = $this->watchErrors();
793 $storeRes = $this->$method( ...$storeArgs );
794 $lastError = $this->getLastError( $watchPoint );
795 if ( $lastError !== self::ERR_NONE ) {
796 $wrapper->setLastError( $lastError );
799 // Convert any store-specific cache keys in the result back to generic cache keys
800 if ( $resSig === self::RES_KEYMAP ) {
801 // Map of (store-specific cache key => generic cache key)
802 $genericKeyByStoreKey = array_combine( $storeArgs[0], $genericArgs[0] );
804 $genericRes = [];
805 foreach ( $storeRes as $storeKey => $value ) {
806 $genericRes[$genericKeyByStoreKey[$storeKey]] = $value;
808 } else {
809 $genericRes = $storeRes;
812 return $genericRes;
816 * @param string $key Key generated by an IStoreKeyEncoder instance
817 * @return string A stats prefix to describe this class of key (e.g. "objectcache.file")
819 protected function determineKeyPrefixForStats( $key ) {
820 $firstComponent = substr( $key, 0, strcspn( $key, ':' ) );
822 $wrapperInfo = $this->wrapperInfoByPrefix[$firstComponent] ?? null;
823 if ( $wrapperInfo ) {
824 // Key has the prefix of a cache wrapper class that wraps BagOStuff
825 $collection = $wrapperInfo[self::WRAPPER_COLLECTION_CALLBACK]( $key );
826 $statsGroup = $wrapperInfo[self::WRAPPER_STATS_GROUP];
827 } else {
828 // Key came directly from BagOStuff::makeKey() or BagOStuff::makeGlobalKey()
829 // and thus has the format of "<scope>:<collection>[:<constant or variable>]..."
830 $components = explode( ':', $key, 3 );
831 // Handle legacy callers that fail to use the key building methods
832 $collection = $components[1] ?? $components[0];
833 $statsGroup = 'objectcache';
836 // Replace dots because they are special in StatsD (T232907)
837 return $statsGroup . '.' . strtr( $collection, '.', '_' );
841 * @internal For testing only
842 * @return float UNIX timestamp
843 * @codeCoverageIgnore
845 public function getCurrentTime() {
846 return $this->wallClockOverride ?: microtime( true );
850 * @internal For testing only
851 * @param float|null &$time Mock UNIX timestamp
852 * @codeCoverageIgnore
854 public function setMockTime( &$time ) {
855 $this->wallClockOverride =& $time;