Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.69% covered (success)
91.69%
706 / 770
67.35% covered (warning)
67.35%
33 / 49
CRAP
0.00% covered (danger)
0.00%
0 / 1
WANObjectCache
91.81% covered (success)
91.81%
706 / 769
67.35% covered (warning)
67.35%
33 / 49
241.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 getMulti
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
5
 fetchKeys
96.92% covered (success)
96.92%
63 / 65
0.00% covered (danger)
0.00%
0 / 1
17
 processCheckKeys
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 set
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 setMainValue
97.44% covered (success)
97.44%
76 / 78
0.00% covered (danger)
0.00%
0 / 1
22
 delete
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 getCheckKeyTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMultiCheckKeyTime
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 touchCheckKey
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 resetCheckKey
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getWithSetCallback
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 fetchOrRegenerate
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
1 / 1
30
 makeSisterKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getInterimValue
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
5.93
 setInterimValue
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 getMultiWithSetCallback
85.19% covered (warning)
85.19%
23 / 27
0.00% covered (danger)
0.00%
0 / 1
3.03
 getMultiWithUnionSetCallback
84.21% covered (warning)
84.21%
48 / 57
0.00% covered (danger)
0.00%
0 / 1
10.39
 makeGlobalKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeMultiKeys
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 multiRemap
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 watchErrors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 clearProcessCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 useInterimHoldOffCaching
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQoS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 adaptiveTTL
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getWarmupKeyMisses
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteKey
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 scheduleAsyncRefresh
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
3.91
 isAcceptablyFreshValue
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 isLotteryRefreshDue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 worthRefreshPopular
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
7.35
 worthRefreshExpiring
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 isValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 wrap
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 unwrap
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
6
 determineKeyGroupForStats
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parsePurgeValue
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 makeCheckPurgeValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getProcessCache
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getNonProcessCachedMultiKeys
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 fetchWrappedValuesForWarmupCache
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
7.05
 timeSinceLoggedMiss
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getCurrentTime
n/a
0 / 0
n/a
0 / 0
2
 setMockTime
n/a
0 / 0
n/a
0 / 0
2
 startOperationSpan
27.78% covered (danger)
27.78%
5 / 18
0.00% covered (danger)
0.00%
0 / 1
14.42
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace Wikimedia\ObjectCache;
8
9use ArrayIterator;
10use Closure;
11use Exception;
12use Psr\Log\LoggerAwareInterface;
13use Psr\Log\LoggerInterface;
14use Psr\Log\NullLogger;
15use RuntimeException;
16use UnexpectedValueException;
17use Wikimedia\LightweightObjectStore\ExpirationAwareness;
18use Wikimedia\Stats\StatsFactory;
19use Wikimedia\Telemetry\NoopTracer;
20use Wikimedia\Telemetry\SpanInterface;
21use Wikimedia\Telemetry\TracerInterface;
22
23/**
24 * Multi-datacenter aware caching interface
25 *
26 * ### Using WANObjectCache
27 *
28 * %WANObjectCache (known as **WANCache**, pronounced whan-cache) improves performance
29 * by reducing database load, increasing web server capacity (fewer repeated computations) and
30 * providing faster access to data. The data cached here follows a "cache-aside" strategy, with
31 * data potentially derived from database rows. Generally speaking, cache data should be treated
32 * as equally up-to-date to data from a replica database, and is thus essentially subject to the
33 * same replication lag.
34 *
35 * The primary way to interact with this class is via the getWithSetCallback() method.
36 *
37 * Each data center has its own cache cluster, with web servers in a given datacenter
38 * populating and reading from the local datacenter only. The exceptions are methods delete(),
39 * touchCheckKey(), and resetCheckKey(), which also asynchronously broadcast the equivalent
40 * purge to other datacenters.
41 *
42 * To learn how this is used and configured at Wikimedia Foundation,
43 * refer to <https://wikitech.wikimedia.org/wiki/Memcached_for_MediaWiki>.
44 *
45 * For broader guidance on how to approach caching in MediaWiki at scale,
46 * refer to <https://wikitech.wikimedia.org/wiki/MediaWiki_Engineering/Guides/Backend_performance_practices>.
47 *
48 * For your code to "see" new values in a timely manner, you need to follow either the
49 * validation strategy, or the purge strategy.
50 *
51 * #### Strategy 1: Validation
52 *
53 * The validation strategy refers to the natural avoidance of stale data
54 * by one of the following means:
55 *
56 *   - A) The cached value is immutable.
57 *
58 *        If you can obtain all the information needed to uniquely describe the value,
59 *        then the value never has to change or be purged. Instead, the key changes,
60 *        which naturally creates a miss where you can compute the right value.
61 *        For example, a transformation like parsing or transforming some input,
62 *        could have a cache key like `example-myparser, option-xyz, v2, hash1234`
63 *        which would describe the transformation, the version/parameters, and a hash
64 *        of the exact input.
65 *
66 *        This also naturally avoids oscillation or corruption in the context of multiple
67 *        servers and data centers, where your code may not always be running the same version
68 *        everywhere at the same time. Newer code would have its own set of cache keys,
69 *        ensuring a deterministic outcome.
70 *   - B) The value is cached with a low TTL.
71 *
72 *        If you can tolerate a few seconds or minutes of delay before changes are reflected
73 *        in the way your data is used, and if re-computation is quick, you can consider
74 *        caching it with a "blind" TTL â€“ using the value's age as your method of validation.
75 *   - C) Validity is checked against an external source.
76 *
77 *        Perhaps you prefer to utilize the old data as fallback or to help compute the new
78 *        value, or for other reasons you need to have a stable key across input changes
79 *        (e.g. cache by page title instead of revision ID). If you put the variable identifier
80 *        (e.g. input hash, or revision ID) in the cache value, and validate this on retrieval
81 *        then you don't need purging or expiration.
82 *
83 *        After calling get() you can validate the ID inside the cached value against what
84 *        you know. When needed, recompute the value and call set().
85 *
86 * #### Strategy 2: Purge
87 *
88 * The purge strategy refers to the approach whereby your application knows that source
89 * data has changed and can react by purging the relevant cache keys.
90 * The simplest purge method is delete().
91 *
92 * Note that cache updates and purges are not immediately visible to all application servers in
93 * all data centers. The cache should be treated like a replica database in this regard.
94 * If immediate synchronization is required, then solutions must be sought outside WANCache.
95 *
96 * Write operations like delete() and the "set" part of getWithSetCallback(), may return true as
97 * soon as the command has been sent or buffered to an open connection to the cache cluster.
98 * It will be processed and/or broadcasted asynchronously.
99 *
100 * @anchor wanobjectcache-deployment
101 * ### Deploying WANObjectCache
102 *
103 * There are two supported ways for sysadmins to set up multi-DC cache purging:
104 *
105 *   - A) Set up mcrouter as the cache backend, with a memcached BagOStuff class for the 'cache'
106 *        parameter, and a wildcard routing prefix for the 'broadcastRoutingPrefix' parameter.
107 *        Configure mcrouter as follows:
108 *          - Define a "<datacenter>" pool of memcached servers for each datacenter.
109 *          - Define a "<datacenter>/wan" route to each datacenter, using "AllSyncRoute" for the
110 *            routes that go to the local datacenter pool and "AllAsyncRoute" for the routes that
111 *            go to remote datacenter pools. The child routes should use "HashRoute|<datacenter>".
112 *            This allows for the use of a wildcard route for 'broadcastRoutingPrefix'. See
113 *            https://github.com/facebook/mcrouter/wiki/Routing-Prefix and
114 *            https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup.
115 *          - In order to reroute operations from "down" servers to spare ("gutter") servers, use
116 *            "FailoverWithExptimeRoute" (failover_exptime=60) instead of "HashRoute|<datacenter>"
117 *            in the "AllSyncRoute"/"AllAsyncRoute" child routes.
118 *            The "gutter" pool is a set of memcached servers that only handle failover traffic.
119 *            Such servers should be carefully spread over different rows and racks. See
120 *            https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles#failoverroute
121 *   - B) Set up dynomite as the cache backend, using a memcached BagOStuff class for the 'cache'
122 *        parameter. Note that with this setup, all key setting operations will be broadcasted,
123 *        rather than just purges. Writes will be eventually consistent via the Dynamo replication
124 *        model. See https://github.com/Netflix/dynomite.
125 *
126 * Broadcasted operations like delete() and touchCheckKey() are intended to run
127 * immediately in the local datacenter and asynchronously in remote datacenters.
128 *
129 * This means that callers in all datacenters may see older values for however many
130 * milliseconds that the purge took to reach that datacenter. As with any cache, this
131 * should not be relied on for cases where reads are used to determine writes to source
132 * (e.g. non-cache) data stores, except when reading immutable data.
133 *
134 * Internally, access to a given key actually involves the use of one or more "sister" keys.
135 * A sister key is constructed by prefixing the base key with "WANCache:" (used to distinguish
136 * WANObjectCache formatted keys) and suffixing a colon followed by a single-character sister
137 * key type. The sister key types include the following:
138 *
139 * - `v`: used to store "regular" values (metadata-wrapped) and temporary purge "tombstones".
140 * - `t`: used to store "last purge" timestamps for "check" keys.
141 * - `m`: used to store temporary mutex locks to avoid cache stampedes.
142 * - `i`: used to store temporary interim values (metadata-wrapped) for tombstoned keys.
143 *
144 * @ingroup Cache
145 * @newable
146 * @since 1.26
147 */
148class WANObjectCache implements
149    ExpirationAwareness,
150    IStoreKeyEncoder,
151    LoggerAwareInterface
152{
153    /** @var BagOStuff The local datacenter cache */
154    protected $cache;
155    /** @var MapCacheLRU[] Map of group PHP instance caches */
156    protected $processCaches = [];
157    /** @var LoggerInterface */
158    protected $logger;
159    /** @var StatsFactory */
160    protected $stats;
161    /** @var callable|null Function that takes a WAN cache callback and runs it later */
162    protected $asyncHandler;
163
164    /**
165     * Routing prefix for operations that should be broadcasted to all data centers.
166     *
167     * If null, the there is only one datacenter or a backend proxy broadcasts everything.
168     *
169     * @var string|null
170     */
171    protected $broadcastRoute;
172    /** @var bool Whether to use "interim" caching while keys are tombstoned */
173    protected $useInterimHoldOffCaching = true;
174    /** @var float Unix timestamp of the oldest possible valid values */
175    protected $epoch;
176    /** @var int Scheme to use for key coalescing (Hash Tags or Hash Stops) */
177    protected $coalesceScheme;
178
179    /** @var TracerInterface */
180    private $tracer;
181
182    /** @var array<int,array> List of (key, UNIX timestamp) tuples for get() cache misses */
183    private $missLog;
184
185    /** @var int Callback stack depth for getWithSetCallback() */
186    private $callbackDepth = 0;
187    /** @var mixed[] Temporary warm-up cache */
188    private $warmupCache = [];
189    /** @var int Key fetched */
190    private $warmupKeyMisses = 0;
191
192    /** @var float|null */
193    private $wallClockOverride;
194
195    /** Max expected seconds to pass between delete() and DB commit finishing */
196    private const MAX_COMMIT_DELAY = 3;
197    /** Max expected seconds of combined lag from replication and "view snapshots" */
198    private const MAX_READ_LAG = 7;
199    /** Seconds to tombstone keys on delete() and to treat keys as volatile after purges */
200    public const HOLDOFF_TTL = self::MAX_COMMIT_DELAY + self::MAX_READ_LAG + 1;
201
202    /** Consider regeneration if the key will expire within this many seconds */
203    private const LOW_TTL = 60;
204    /** Max TTL, in seconds, to store keys when a data source has high replication lag */
205    public const TTL_LAGGED = 30;
206
207    /** Expected time-till-refresh, in seconds, if the key is accessed once per second */
208    private const HOT_TTR = 900;
209    /** Minimum key age, in seconds, for expected time-till-refresh to be considered */
210    private const AGE_NEW = 60;
211
212    /** Idiom for getWithSetCallback() meaning "no cache stampede mutex" */
213    private const TSE_NONE = -1;
214
215    /** Idiom for set()/getWithSetCallback() meaning "no post-expiration persistence" */
216    public const STALE_TTL_NONE = 0;
217    /** Idiom for set()/getWithSetCallback() meaning "no post-expiration grace period" */
218    public const GRACE_TTL_NONE = 0;
219    /** Idiom for delete()/touchCheckKey() meaning "no hold-off period" */
220    public const HOLDOFF_TTL_NONE = 0;
221
222    /** @var float Idiom for getWithSetCallback() meaning "no minimum required as-of timestamp" */
223    public const MIN_TIMESTAMP_NONE = 0.0;
224
225    /** Default process cache name and max key count */
226    private const PC_PRIMARY = 'primary:1000';
227
228    /** Idiom for get()/getMulti() to return extra information by reference */
229    public const PASS_BY_REF = [];
230
231    /** Use twemproxy-style Hash Tag key scheme (e.g. "{...}") */
232    private const SCHEME_HASH_TAG = 1;
233    /** Use mcrouter-style Hash Stop key scheme (e.g. "...|#|") */
234    private const SCHEME_HASH_STOP = 2;
235
236    /** Seconds to keep dependency purge keys around */
237    private const CHECK_KEY_TTL = self::TTL_YEAR;
238    /** Seconds to keep interim value keys for tombstoned keys around */
239    private const INTERIM_KEY_TTL = 2;
240
241    /** Seconds to keep lock keys around */
242    private const LOCK_TTL = 10;
243    /** Seconds to ramp up the chance of regeneration due to expected time-till-refresh */
244    private const RAMPUP_TTL = 30;
245
246    /** @var float Tiny negative float to use when CTL comes up >= 0 due to clock skew */
247    private const TINY_NEGATIVE = -0.000001;
248    /** @var float Tiny positive float to use when using "minTime" to assert an inequality */
249    private const TINY_POSITIVE = 0.000001;
250
251    /** Min millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */
252    private const RECENT_SET_LOW_MS = 50;
253    /** Max millisecond set() backoff during hold-off (far less than INTERIM_KEY_TTL) */
254    private const RECENT_SET_HIGH_MS = 100;
255
256    /** Consider value generation somewhat high if it takes this many seconds or more */
257    private const GENERATION_HIGH_SEC = 0.2;
258
259    /** Key to the tombstone entry timestamp */
260    private const PURGE_TIME = 0;
261    /** Key to the tombstone entry hold-off TTL */
262    private const PURGE_HOLDOFF = 1;
263
264    /** Cache format version number */
265    private const VERSION = 1;
266
267    /** Version number attribute for a key; keep value for b/c (< 1.36) */
268    public const KEY_VERSION = 'version';
269    /** Generation completion timestamp attribute for a key; keep value for b/c (< 1.36) */
270    public const KEY_AS_OF = 'asOf';
271    /** Logical TTL attribute for a key */
272    public const KEY_TTL = 'ttl';
273    /** Remaining TTL attribute for a key; keep value for b/c (< 1.36) */
274    public const KEY_CUR_TTL = 'curTTL';
275    /** Tombstone timestamp attribute for a key; keep value for b/c (< 1.36) */
276    public const KEY_TOMB_AS_OF = 'tombAsOf';
277    /** Highest "check" key timestamp for a key; keep value for b/c (< 1.36) */
278    public const KEY_CHECK_AS_OF = 'lastCKPurge';
279
280    /** Value for a key */
281    private const RES_VALUE = 0;
282    /** Version number attribute for a key */
283    private const RES_VERSION = 1;
284    /** Generation completion timestamp attribute for a key */
285    private const RES_AS_OF = 2;
286    /** Logical TTL attribute for a key */
287    private const RES_TTL = 3;
288    /** Tombstone timestamp attribute for a key */
289    private const RES_TOMB_AS_OF = 4;
290    /** Highest "check" key timestamp for a key */
291    private const RES_CHECK_AS_OF = 5;
292    /** Highest "touched" timestamp for a key */
293    private const RES_TOUCH_AS_OF = 6;
294    /** Remaining TTL attribute for a key */
295    private const RES_CUR_TTL = 7;
296
297    /** Key to WAN cache version number; stored in blobs */
298    private const FLD_FORMAT_VERSION = 0;
299    /** Key to the cached value; stored in blobs */
300    private const FLD_VALUE = 1;
301    /** Key to the original TTL; stored in blobs */
302    private const FLD_TTL = 2;
303    /** Key to the cache timestamp; stored in blobs */
304    private const FLD_TIME = 3;
305    /** Key to the flags bit field (reserved number) */
306    private const /** @noinspection PhpUnusedPrivateFieldInspection */ FLD_FLAGS = 4;
307    /** Key to collection cache version number; stored in blobs */
308    private const FLD_VALUE_VERSION = 5;
309    private const /** @noinspection PhpUnusedPrivateFieldInspection */ FLD_GENERATION_TIME = 6;
310
311    /** Single character component for value keys */
312    private const TYPE_VALUE = 'v';
313    /** Single character component for timestamp check keys */
314    private const TYPE_TIMESTAMP = 't';
315    /** Single character component for mutex lock keys */
316    private const TYPE_MUTEX = 'm';
317    /** Single character component for interim value keys */
318    private const TYPE_INTERIM = 'i';
319
320    /** Value prefix of purge values */
321    private const PURGE_VAL_PREFIX = 'PURGED';
322
323    /**
324     * @stable to call
325     * @param array $params
326     *   - cache    : BagOStuff object for a persistent cache
327     *   - logger   : LoggerInterface object
328     *   - stats    : StatsFactory object. Since 1.43, constructing a WANObjectCache object
329     *       with an IBufferingStatsdDataFactory stats collector will emit a warning.
330     *   - asyncHandler : A function that takes a callback and runs it later. If supplied,
331     *       whenever a preemptive refresh would be triggered in getWithSetCallback(), the
332     *       current cache value is still used instead. However, the async-handler function
333     *       receives a WAN cache callback that, when run, will execute the value generation
334     *       callback supplied by the getWithSetCallback() caller. The result will be saved
335     *       as normal. The handler is expected to call the WAN cache callback at an opportune
336     *       time (e.g. HTTP post-send), though generally within a few 100ms. [optional]
337     *   - broadcastRoutingPrefix: a routing prefix used to broadcast certain operations to all
338     *       datacenters; See also <https://github.com/facebook/mcrouter/wiki/Config-Files>.
339     *       This prefix takes the form `/<datacenter>/<name of wan route>/`, where `datacenter`
340     *       is usually a wildcard to select all matching routes (e.g. the WAN cluster in all DCs).
341     *       See also <https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup>.
342     *       This is required when using mcrouter as a multi-region backing store proxy. [optional]
343     *   - epoch: lowest UNIX timestamp a value/tombstone must have to be valid. [optional]
344     *   - coalesceScheme: which key scheme to use in order to encourage the backend to place any
345     *       "helper" keys for a "value" key within the same cache server. This reduces network
346     *       overhead and reduces the chance the single downed cache server causes disruption.
347     *       Use "hash_stop" with mcrouter and "hash_tag" with dynomite. [default: "hash_stop"]
348     *   - tracer: TracerInterface instance where per-operation spans will be recorded
349     */
350    public function __construct( array $params ) {
351        $this->cache = $params['cache'];
352        $this->broadcastRoute = $params['broadcastRoutingPrefix'] ?? null;
353        $this->epoch = $params['epoch'] ?? 0;
354        if ( ( $params['coalesceScheme'] ?? '' ) === 'hash_tag' ) {
355            // https://redis.io/topics/cluster-spec
356            // https://github.com/twitter/twemproxy/blob/v0.4.1/notes/recommendation.md#hash-tags
357            // https://github.com/Netflix/dynomite/blob/v0.7.0/notes/recommendation.md#hash-tags
358            $this->coalesceScheme = self::SCHEME_HASH_TAG;
359        } else {
360            // https://github.com/facebook/mcrouter/wiki/Key-syntax
361            $this->coalesceScheme = self::SCHEME_HASH_STOP;
362        }
363
364        $this->setLogger( $params['logger'] ?? new NullLogger() );
365        $this->tracer = $params['tracer'] ?? new NoopTracer();
366        $this->stats = $params['stats'] ?? StatsFactory::newNull();
367
368        $this->asyncHandler = $params['asyncHandler'] ?? null;
369        $this->missLog = array_fill( 0, 10, [ '', 0.0 ] );
370    }
371
372    public function setLogger( LoggerInterface $logger ): void {
373        $this->logger = $logger;
374    }
375
376    /**
377     * Get an instance that wraps EmptyBagOStuff
378     */
379    public static function newEmpty(): static {
380        return new static( [ 'cache' => new EmptyBagOStuff() ] );
381    }
382
383    /**
384     * Fetch the value of a key from cache
385     *
386     * If supplied, $curTTL is set to the remaining TTL (current time left):
387     *   - a) INF; if $key exists, has no TTL, and is not purged by $checkKeys
388     *   - b) float (>=0); if $key exists, has a TTL, and is not purged by $checkKeys
389     *   - c) float (<0); if $key is tombstoned, stale, or existing but purged by $checkKeys
390     *   - d) null; if $key does not exist and is not tombstoned
391     *
392     * If a key is tombstoned, $curTTL will reflect the time since delete().
393     *
394     * The timestamp of $key will be checked against the last-purge timestamp
395     * of each of $checkKeys. Those $checkKeys not in cache will have the last-purge
396     * initialized to the current timestamp. If any of $checkKeys have a timestamp
397     * greater than that of $key, then $curTTL will reflect how long ago $key
398     * became invalid. Callers can use $curTTL to know when the value is stale.
399     * The $checkKeys parameter allow mass key purges by updating a single key:
400     *   - a) Each "check" key represents "last purged" of some source data
401     *   - b) Callers pass in relevant "check" keys as $checkKeys in get()
402     *   - c) When the source data that "check" keys represent changes,
403     *        the touchCheckKey() method is called on them
404     *
405     * Source data entities might exist in a DB that uses snapshot isolation
406     * (e.g. the default REPEATABLE-READ in innoDB). Even for mutable data, that
407     * isolation can largely be maintained by doing the following:
408     *   - a) Calling delete() on entity change *and* creation, before DB commit
409     *   - b) Keeping transaction duration shorter than the delete() hold-off TTL
410     *   - c) Disabling interim key caching via useInterimHoldOffCaching() before get() calls
411     *
412     * However, pre-snapshot values might still be seen if an update was made
413     * in a remote datacenter but the purge from delete() didn't relay yet.
414     *
415     * Consider using getWithSetCallback(), which has cache slam avoidance and key
416     * versioning features, instead of bare get()/set() calls.
417     *
418     * Do not use this method on versioned keys accessed via getWithSetCallback().
419     *
420     * When using the $info parameter, it should be passed in as WANObjectCache::PASS_BY_REF.
421     * In that case, it becomes a key metadata map. Otherwise, for backwards compatibility,
422     * $info becomes the value generation timestamp (null if the key is nonexistant/tombstoned).
423     * Key metadata map fields include:
424     *   - WANObjectCache::KEY_VERSION: value version number; null if key is nonexistant
425     *   - WANObjectCache::KEY_AS_OF: value generation timestamp (UNIX); null if key is nonexistant
426     *   - WANObjectCache::KEY_TTL: assigned TTL (seconds); null if key is nonexistant/tombstoned
427     *   - WANObjectCache::KEY_CUR_TTL: remaining TTL (seconds); null if key is nonexistant
428     *   - WANObjectCache::KEY_TOMB_AS_OF: tombstone timestamp (UNIX); null if key is not tombstoned
429     *   - WANObjectCache::KEY_CHECK_AS_OF: highest "check" key timestamp (UNIX); null if none
430     *
431     * @param string $key Cache key made with makeKey()/makeGlobalKey()
432     * @param float|null &$curTTL Seconds of TTL left [returned]
433     * @param string[] $checkKeys Map of (integer or cache key => "check" key(s));
434     *  "check" keys must also be made with makeKey()/makeGlobalKey()
435     * @param array &$info Metadata map [returned]
436     * @return mixed Value of cache key; false on failure
437     */
438    final public function get( $key, &$curTTL = null, array $checkKeys = [], &$info = [] ) {
439        // Note that an undeclared variable passed as $info starts as null (not the default).
440        // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
441        $legacyInfo = ( $info !== self::PASS_BY_REF );
442
443        /** @noinspection PhpUnusedLocalVariableInspection */
444        $span = $this->startOperationSpan( __FUNCTION__, $key, $checkKeys );
445
446        $now = $this->getCurrentTime();
447        $res = $this->fetchKeys( [ $key ], $checkKeys, $now )[$key];
448
449        $curTTL = $res[self::RES_CUR_TTL];
450        $info = $legacyInfo
451            ? $res[self::RES_AS_OF]
452            : [
453                self::KEY_VERSION => $res[self::RES_VERSION],
454                self::KEY_AS_OF => $res[self::RES_AS_OF],
455                self::KEY_TTL => $res[self::RES_TTL],
456                self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
457                self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
458                self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
459            ];
460
461        if ( $curTTL === null || $curTTL <= 0 ) {
462            // Log the timestamp in case a corresponding set() call does not provide "walltime"
463            unset( $this->missLog[array_key_first( $this->missLog )] );
464            $this->missLog[] = [ $key, $this->getCurrentTime() ];
465        }
466
467        return $res[self::RES_VALUE];
468    }
469
470    /**
471     * Fetch the value of several keys from cache
472     *
473     * $curTTLs becomes a map of only present/tombstoned $keys to their current time-to-live.
474     *
475     * $checkKeys holds the "check" keys used to validate values of applicable keys. The
476     * integer indexes hold "check" keys that apply to all of $keys while the string indexes
477     * hold "check" keys that only apply to the cache key with that name. The logic of "check"
478     * keys otherwise works the same as in WANObjectCache::get().
479     *
480     * When using the $info parameter, it should be passed in as WANObjectCache::PASS_BY_REF.
481     * In that case, it becomes a mapping of all the $keys to their metadata maps, each in the
482     * style of WANObjectCache::get(). Otherwise, for backwards compatibility, $info becomes a
483     * map of only present/tombstoned $keys to their value generation timestamps.
484     *
485     * @see WANObjectCache::get()
486     *
487     * @param string[] $keys List/map with makeKey()/makeGlobalKey() cache keys as values
488     * @param array<string,float> &$curTTLs Map of (key => seconds of TTL left) [returned]
489     * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s));
490     *  "check" keys must also be made with makeKey()/makeGlobalKey()
491     * @param array<string,array> &$info Map of (key => metadata map) [returned]
492     * @return array<string,mixed> Map of (key => value) for existing values in order of $keys
493     */
494    final public function getMulti(
495        array $keys,
496        &$curTTLs = [],
497        array $checkKeys = [],
498        &$info = []
499    ) {
500        // Note that an undeclared variable passed as $info starts as null (not the default).
501        // Also, if no $info parameter is provided, then it doesn't matter how it changes here.
502        $legacyInfo = ( $info !== self::PASS_BY_REF );
503
504        /** @noinspection PhpUnusedLocalVariableInspection */
505        $span = $this->startOperationSpan( __FUNCTION__, $keys, $checkKeys );
506
507        $curTTLs = [];
508        $info = [];
509        $valuesByKey = [];
510
511        $now = $this->getCurrentTime();
512        $resByKey = $this->fetchKeys( $keys, $checkKeys, $now );
513        foreach ( $resByKey as $key => $res ) {
514            if ( $res[self::RES_VALUE] !== false ) {
515                $valuesByKey[$key] = $res[self::RES_VALUE];
516            }
517
518            if ( $res[self::RES_CUR_TTL] !== null ) {
519                $curTTLs[$key] = $res[self::RES_CUR_TTL];
520            }
521            $info[$key] = $legacyInfo
522                ? $res[self::RES_AS_OF]
523                : [
524                    self::KEY_VERSION => $res[self::RES_VERSION],
525                    self::KEY_AS_OF => $res[self::RES_AS_OF],
526                    self::KEY_TTL => $res[self::RES_TTL],
527                    self::KEY_CUR_TTL => $res[self::RES_CUR_TTL],
528                    self::KEY_TOMB_AS_OF => $res[self::RES_TOMB_AS_OF],
529                    self::KEY_CHECK_AS_OF => $res[self::RES_CHECK_AS_OF]
530                ];
531        }
532
533        return $valuesByKey;
534    }
535
536    /**
537     * Fetch the value and key metadata of several keys from cache
538     *
539     * $checkKeys holds the "check" keys used to validate values of applicable keys.
540     * The integer indexes hold "check" keys that apply to all of $keys while the string
541     * indexes hold "check" keys that only apply to the cache key with that name.
542     *
543     * @param string[] $keys List/map with makeKey()/makeGlobalKey() cache keys as values
544     * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s));
545     *  "check" keys must also be made with makeKey()/makeGlobalKey()
546     * @param float $now The current UNIX timestamp
547     * @param callable|null $touchedCb Callback yielding a UNIX timestamp from a value, or null
548     * @return array<string,array> Map of (key => WANObjectCache::RESULT_* map) in order of $keys
549     * @note Callable type hints are not used to avoid class-autoloading
550     */
551    protected function fetchKeys( array $keys, array $checkKeys, float $now, $touchedCb = null ) {
552        $resByKey = [];
553
554        // List of all sister keys that need to be fetched from cache
555        $allSisterKeys = [];
556        // Order-corresponding value sister key list for the base key list ($keys)
557        $valueSisterKeys = [];
558        // List of "check" sister keys to compare all value sister keys against
559        $checkSisterKeysForAll = [];
560        // Map of (base key => additional "check" sister key(s) to compare against)
561        $checkSisterKeysByKey = [];
562
563        foreach ( $keys as $key ) {
564            $sisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
565            $allSisterKeys[] = $sisterKey;
566            $valueSisterKeys[] = $sisterKey;
567        }
568
569        foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
570            // Note: avoid array_merge() inside loop in case there are many keys
571            if ( is_int( $i ) ) {
572                // Single "check" key that applies to all base keys
573                $sisterKey = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
574                $allSisterKeys[] = $sisterKey;
575                $checkSisterKeysForAll[] = $sisterKey;
576            } else {
577                // List of "check" keys that apply to a specific base key
578                foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
579                    $sisterKey = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
580                    $allSisterKeys[] = $sisterKey;
581                    $checkSisterKeysByKey[$i][] = $sisterKey;
582                }
583            }
584        }
585
586        if ( $this->warmupCache ) {
587            // Get the wrapped values of the sister keys from the warmup cache
588            $wrappedBySisterKey = $this->warmupCache;
589            $sisterKeysMissing = array_diff( $allSisterKeys, array_keys( $wrappedBySisterKey ) );
590            if ( $sisterKeysMissing ) {
591                $this->warmupKeyMisses += count( $sisterKeysMissing );
592                $wrappedBySisterKey += $this->cache->getMulti( $sisterKeysMissing );
593            }
594        } else {
595            // Fetch the wrapped values of the sister keys from the backend
596            $wrappedBySisterKey = $this->cache->getMulti( $allSisterKeys );
597        }
598
599        // List of "check" sister key purge timestamps to compare all value sister keys against
600        $ckPurgesForAll = $this->processCheckKeys(
601            $checkSisterKeysForAll,
602            $wrappedBySisterKey,
603            $now
604        );
605        // Map of (base key => extra "check" sister key purge timestamp(s) to compare against)
606        $ckPurgesByKey = [];
607        foreach ( $checkSisterKeysByKey as $keyWithCheckKeys => $checkKeysForKey ) {
608            $ckPurgesByKey[$keyWithCheckKeys] = $this->processCheckKeys(
609                $checkKeysForKey,
610                $wrappedBySisterKey,
611                $now
612            );
613        }
614
615        // Unwrap and validate any value found for each base key (under the value sister key)
616        foreach (
617            array_map( null, $valueSisterKeys, $keys )
618                as [ $valueSisterKey, $key ]
619        ) {
620            if ( array_key_exists( $valueSisterKey, $wrappedBySisterKey ) ) {
621                // Key exists as either a live value or tombstone value
622                $wrapped = $wrappedBySisterKey[$valueSisterKey];
623            } else {
624                // Key does not exist
625                $wrapped = false;
626            }
627
628            $res = $this->unwrap( $wrapped, $now );
629            $value = $res[self::RES_VALUE];
630
631            foreach ( array_merge( $ckPurgesForAll, $ckPurgesByKey[$key] ?? [] ) as $ckPurge ) {
632                $res[self::RES_CHECK_AS_OF] = max(
633                    $ckPurge[self::PURGE_TIME],
634                    $res[self::RES_CHECK_AS_OF]
635                );
636                // Timestamp marking the end of the hold-off period for this purge
637                $holdoffDeadline = $ckPurge[self::PURGE_TIME] + $ckPurge[self::PURGE_HOLDOFF];
638                // Check if the value was generated during the hold-off period
639                if ( $value !== false && $holdoffDeadline >= $res[self::RES_AS_OF] ) {
640                    // How long ago this value was purged by *this* "check" key
641                    $ago = min( $ckPurge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
642                    // How long ago this value was purged by *any* known "check" key
643                    $res[self::RES_CUR_TTL] = min( $res[self::RES_CUR_TTL], $ago );
644                }
645            }
646
647            if ( $touchedCb !== null && $value !== false ) {
648                $touched = $touchedCb( $value );
649                if ( $touched !== null && $touched >= $res[self::RES_AS_OF] ) {
650                    $res[self::RES_CUR_TTL] = min(
651                        $res[self::RES_CUR_TTL],
652                        $res[self::RES_AS_OF] - $touched,
653                        self::TINY_NEGATIVE
654                    );
655                }
656            } else {
657                $touched = null;
658            }
659
660            $res[self::RES_TOUCH_AS_OF] = max( $res[self::RES_TOUCH_AS_OF], $touched );
661
662            $resByKey[$key] = $res;
663        }
664
665        return $resByKey;
666    }
667
668    /**
669     * @param string[] $checkSisterKeys List of "check" sister keys
670     * @param mixed[] $wrappedBySisterKey Preloaded map of (sister key => wrapped value)
671     * @param float $now UNIX timestamp
672     * @return array[] List of purge value arrays
673     */
674    private function processCheckKeys(
675        array $checkSisterKeys,
676        array $wrappedBySisterKey,
677        float $now
678    ) {
679        $purges = [];
680
681        foreach ( $checkSisterKeys as $timeKey ) {
682            $purge = isset( $wrappedBySisterKey[$timeKey] )
683                ? $this->parsePurgeValue( $wrappedBySisterKey[$timeKey] )
684                : null;
685
686            if ( $purge === null ) {
687                // No holdoff when lazy creating a check key, use cache right away (T344191)
688                $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
689                $this->cache->add(
690                    $timeKey,
691                    $wrapped,
692                    self::CHECK_KEY_TTL,
693                    $this->cache::WRITE_BACKGROUND
694                );
695            }
696
697            $purges[] = $purge;
698        }
699
700        return $purges;
701    }
702
703    /**
704     * Set the value of a key in cache
705     *
706     * Simply calling this method when source data changes is not valid because
707     * the changes do not replicate to the other WAN sites. In that case, delete()
708     * should be used instead. This method is intended for use on cache misses.
709     *
710     * If data was read using "view snapshots" (e.g. innodb REPEATABLE-READ),
711     * use 'since' to avoid the following race condition:
712     *   - a) T1 starts
713     *   - b) T2 updates a row, calls delete(), and commits
714     *   - c) The HOLDOFF_TTL passes, expiring the delete() tombstone
715     *   - d) T1 reads the row and calls set() due to a cache miss
716     *   - e) Stale value is stuck in cache
717     *
718     * Setting 'lag' and 'since' help avoids keys getting stuck in stale states.
719     *
720     * Be aware that this does not update the process cache for getWithSetCallback()
721     * callers. Keys accessed via that method are not generally meant to also be set
722     * using this primitive method.
723     *
724     * Consider using getWithSetCallback(), which has cache slam avoidance and key
725     * versioning features, instead of bare get()/set() calls.
726     *
727     * Do not use this method on versioned keys accessed via getWithSetCallback().
728     *
729     * Example usage:
730     * @code
731     *     $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
732     *     $setOpts = Database::getCacheSetOptions( $dbr );
733     *     // Fetch the row from the DB
734     *     $row = $dbr->selectRow( ... );
735     *     $key = $cache->makeKey( 'building', $buildingId );
736     *     $cache->set( $key, $row, $cache::TTL_DAY, $setOpts );
737     * @endcode
738     *
739     * @param string $key Cache key made with makeKey()/makeGlobalKey()
740     * @param mixed $value Value to set for the cache key
741     * @param int $ttl Seconds to live. Special values are:
742     *   - WANObjectCache::TTL_INDEFINITE: Cache forever (default)
743     *   - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
744     * @param array $opts Options map:
745     *   - lag: Highest seconds of replication lag potentially affecting reads used to generate
746     *      the value. This should not be affected by the duration of transaction "view snapshots"
747     *      (e.g. innodb REPEATABLE-READ) nor the time elapsed since the first read (though both
748     *      increase staleness). For reads using view snapshots, only the replication lag during
749     *      snapshot initialization matters. Use false if replication is stopped/broken on a
750     *      replica server involved in the reads.
751     *      Default: 0 seconds
752     *   - since: UNIX timestamp indicative of the highest possible staleness caused by the
753     *      duration of transaction "view snapshots" (e.g. innodb REPEATABLE-READ) and the time
754     *      elapsed since the first read. This should not be affected by replication lag.
755     *      Default: 0 seconds
756     *   - pending: Whether this data is possibly from an uncommitted write transaction.
757     *      Generally, other threads should not see values from the future and
758     *      they certainly should not see ones that ended up getting rolled back.
759     *      Default: false
760     *   - lockTSE: If excessive replication/snapshot lag is detected, then store the value
761     *      with this TTL and flag it as stale. This is only useful if the reads for this key
762     *      use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set
763     *      then it will still add on to this TTL in the excessive lag scenario.
764     *      Default: WANObjectCache::TSE_NONE
765     *   - staleTTL: Seconds to keep the key around if it is stale. The get()/getMulti()
766     *      methods return such stale values with a $curTTL of 0, and getWithSetCallback()
767     *      will call the generation callback in such cases, passing in the old value
768     *      and its as-of time to the callback. This is useful if adaptiveTTL() is used
769     *      on the old value's as-of time when it is verified as still being correct.
770     *      Default: WANObjectCache::STALE_TTL_NONE
771     *   - segmentable: Allow partitioning of the value if it is a large string.
772     *      Default: false.
773     *   - creating: Optimize for the case where the key does not already exist.
774     *      Default: false
775     *   - version: Integer version number signifying the format of the value.
776     *      Default: null
777     *   - walltime: How long the value took to generate in seconds. Default: null
778     * @phpcs:ignore Generic.Files.LineLength
779     * @phan-param array{lag?:float|int,since?:float|int,pending?:bool,lockTSE?:int,staleTTL?:int,creating?:bool,version?:int,walltime?:int|float,segmentable?:bool} $opts
780     * @note Options added in 1.28: staleTTL
781     * @note Options added in 1.33: creating
782     * @note Options added in 1.34: version, walltime
783     * @note Options added in 1.40: segmentable
784     * @return bool Success
785     */
786    final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
787        /** @noinspection PhpUnusedLocalVariableInspection */
788        $span = $this->startOperationSpan( __FUNCTION__, $key );
789
790        $keygroup = $this->determineKeyGroupForStats( $key );
791
792        $ok = $this->setMainValue(
793            $key,
794            $value,
795            $ttl,
796            $opts['version'] ?? null,
797            $opts['walltime'] ?? null,
798            $opts['lag'] ?? 0,
799            $opts['since'] ?? null,
800            $opts['pending'] ?? false,
801            $opts['lockTSE'] ?? self::TSE_NONE,
802            $opts['staleTTL'] ?? self::STALE_TTL_NONE,
803            $opts['segmentable'] ?? false,
804            $opts['creating'] ?? false
805        );
806
807        $this->stats->getCounter( 'wanobjectcache_set_total' )
808            ->setLabel( 'keygroup', $keygroup )
809            ->setLabel( 'result', ( $ok ? 'ok' : 'error' ) )
810            ->increment();
811
812        return $ok;
813    }
814
815    /**
816     * @param string $key Cache key made with makeKey()/makeGlobalKey()
817     * @param mixed $value
818     * @param int|float $ttl
819     * @param int|null $version
820     * @param float|null $walltime
821     * @param float|int|bool $dataReplicaLag
822     * @param float|int|null $dataReadSince
823     * @param bool $dataPendingCommit
824     * @param int $lockTSE
825     * @param int $staleTTL
826     * @param bool $segmentable
827     * @param bool $creating
828     * @return bool Success
829     */
830    private function setMainValue(
831        $key,
832        $value,
833        $ttl,
834        ?int $version,
835        ?float $walltime,
836        $dataReplicaLag,
837        $dataReadSince,
838        bool $dataPendingCommit,
839        int $lockTSE,
840        int $staleTTL,
841        bool $segmentable,
842        bool $creating
843    ) {
844        if ( $ttl < 0 ) {
845            // not cacheable
846            return true;
847        }
848
849        $now = $this->getCurrentTime();
850
851        // T413673: Handle PHP8.5 case where TTL is infinite.
852        if ( is_finite( $ttl ) ) {
853            $ttl = (int)$ttl;
854        } else {
855            $ttl = self::TTL_INDEFINITE;
856        }
857        $walltime ??= $this->timeSinceLoggedMiss( $key, $now );
858        $dataSnapshotLag = ( $dataReadSince !== null ) ? max( 0, $now - $dataReadSince ) : 0;
859        $dataCombinedLag = $dataReplicaLag + $dataSnapshotLag;
860
861        // Forbid caching data that only exists within an uncommitted transaction. Also, lower
862        // the TTL when the data has a "since" time so far in the past that a delete() tombstone,
863        // made after that time, could have already expired (the key is no longer write-holed).
864        // The mitigation TTL depends on whether this data lag is assumed to systemically effect
865        // regeneration attempts in the near future. The TTL also reflects regeneration wall time.
866        if ( $dataPendingCommit ) {
867            // Case A: data comes from an uncommitted write transaction
868            $mitigated = 'pending writes';
869            // Data might never be committed; rely on a less problematic regeneration attempt
870            $mitigationTTL = self::TTL_UNCACHEABLE;
871        } elseif ( $dataSnapshotLag > self::MAX_READ_LAG ) {
872            // Case B: high snapshot lag
873            $pregenSnapshotLag = ( $walltime !== null ) ? ( $dataSnapshotLag - $walltime ) : 0;
874            if ( ( $pregenSnapshotLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
875                // Case B1: generation started when transaction duration was already long
876                $mitigated = 'snapshot lag (late generation)';
877                // Probably non-systemic; rely on a less problematic regeneration attempt
878                $mitigationTTL = self::TTL_UNCACHEABLE;
879            } else {
880                // Case B2: slow generation made transaction duration long
881                $mitigated = 'snapshot lag (high generation time)';
882                // Probably systemic; use a low TTL to avoid stampedes/uncacheability
883                $mitigationTTL = self::TTL_LAGGED;
884            }
885        } elseif ( $dataReplicaLag === false || $dataReplicaLag > self::MAX_READ_LAG ) {
886            // Case C: low/medium snapshot lag with high replication lag
887            $mitigated = 'replication lag';
888            // Probably systemic; use a low TTL to avoid stampedes/uncacheability
889            $mitigationTTL = self::TTL_LAGGED;
890        } elseif ( $dataCombinedLag > self::MAX_READ_LAG ) {
891            $pregenCombinedLag = ( $walltime !== null ) ? ( $dataCombinedLag - $walltime ) : 0;
892            // Case D: medium snapshot lag with medium replication lag
893            if ( ( $pregenCombinedLag + self::GENERATION_HIGH_SEC ) > self::MAX_READ_LAG ) {
894                // Case D1: generation started when read lag was too high
895                $mitigated = 'read lag (late generation)';
896                // Probably non-systemic; rely on a less problematic regeneration attempt
897                $mitigationTTL = self::TTL_UNCACHEABLE;
898            } else {
899                // Case D2: slow generation made read lag too high
900                $mitigated = 'read lag (high generation time)';
901                // Probably systemic; use a low TTL to avoid stampedes/uncacheability
902                $mitigationTTL = self::TTL_LAGGED;
903            }
904        } else {
905            // Case E: new value generated with recent data
906            $mitigated = null;
907            // Nothing to mitigate
908            $mitigationTTL = null;
909        }
910
911        if ( $mitigationTTL === self::TTL_UNCACHEABLE ) {
912            $this->logger->warning(
913                "Rejected set() for {cachekey} due to $mitigated.",
914                [
915                    'cachekey' => $key,
916                    'lag' => $dataReplicaLag,
917                    'age' => $dataSnapshotLag,
918                    'walltime' => $walltime
919                ]
920            );
921
922            // no-op the write for being unsafe
923            return true;
924        }
925
926        // TTL to use in staleness checks (does not effect persistence layer TTL)
927        $logicalTTL = null;
928
929        if ( $mitigationTTL !== null ) {
930            // New value was generated from data that is old enough to be risky
931            if ( $lockTSE >= 0 ) {
932                // Persist the value as long as normal, but make it count as stale sooner
933                $logicalTTL = min( $ttl ?: INF, $mitigationTTL );
934            } else {
935                // Persist the value for a shorter duration
936                $ttl = min( $ttl ?: INF, $mitigationTTL );
937            }
938
939            $this->logger->warning(
940                "Lowered set() TTL for {cachekey} due to $mitigated.",
941                [
942                    'cachekey' => $key,
943                    'lag' => $dataReplicaLag,
944                    'age' => $dataSnapshotLag,
945                    'walltime' => $walltime
946                ]
947            );
948        }
949
950        // Wrap that value with time/TTL/version metadata
951        $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $version, $now );
952        $storeTTL = $ttl + $staleTTL;
953
954        $flags = $this->cache::WRITE_BACKGROUND;
955        if ( $segmentable ) {
956            $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
957        }
958
959        if ( $creating ) {
960            $ok = $this->cache->add(
961                $this->makeSisterKey( $key, self::TYPE_VALUE ),
962                $wrapped,
963                $storeTTL,
964                $flags
965            );
966        } else {
967            $ok = $this->cache->merge(
968                $this->makeSisterKey( $key, self::TYPE_VALUE ),
969                static function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
970                    // A string value means that it is a tombstone; do nothing in that case
971                    return ( is_string( $cWrapped ) ) ? false : $wrapped;
972                },
973                $storeTTL,
974                $this->cache::MAX_CONFLICTS_ONE,
975                $flags
976            );
977        }
978
979        return $ok;
980    }
981
982    /**
983     * Purge a key from all datacenters
984     *
985     * This should only be called when the underlying data (being cached)
986     * changes in a significant way. This deletes the key and starts a hold-off
987     * period where the key cannot be written to for a few seconds (HOLDOFF_TTL).
988     * This is done to avoid the following race condition:
989     *   - a) Some DB data changes and delete() is called on a corresponding key
990     *   - b) A request refills the key with a stale value from a lagged DB
991     *   - c) The stale value is stuck there until the key is expired/evicted
992     *
993     * This is implemented by storing a special "tombstone" value at the cache
994     * key that this class recognizes; get() calls will return false for the key
995     * and any set() calls will refuse to replace tombstone values at the key.
996     * For this to always avoid stale value writes, the following must hold:
997     *   - a) Replication lag is bounded to being less than HOLDOFF_TTL; or
998     *   - b) If lag is higher, the DB will have gone into read-only mode already
999     *
1000     * Note that set() can also be lag-aware and lower the TTL if it's high.
1001     *
1002     * Be aware that this does not clear the process cache. Even if it did, callbacks
1003     * used by getWithSetCallback() might still return stale data in the case of either
1004     * uncommitted or not-yet-replicated changes (callback generally use replica DBs).
1005     *
1006     * When using potentially long-running ACID transactions, a good pattern is
1007     * to use a pre-commit hook to issue the delete(). This means that immediately
1008     * after commit, callers will see the tombstone in cache upon purge relay.
1009     * It also avoids the following race condition:
1010     *   - a) T1 begins, changes a row, and calls delete()
1011     *   - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
1012     *   - c) T2 starts, reads the row and calls set() due to a cache miss
1013     *   - d) T1 finally commits
1014     *   - e) Stale value is stuck in cache
1015     *
1016     * Example usage:
1017     * @code
1018     *     $dbw->startAtomic( __METHOD__ ); // start of request
1019     *     ... <execute some stuff> ...
1020     *     // Update the row in the DB
1021     *     $dbw->update( ... );
1022     *     $key = $cache->makeKey( 'homes', $homeId );
1023     *     // Purge the corresponding cache entry just before committing
1024     *     $dbw->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) {
1025     *         $cache->delete( $key );
1026     *     } );
1027     *     ... <execute some stuff> ...
1028     *     $dbw->endAtomic( __METHOD__ ); // end of request
1029     * @endcode
1030     *
1031     * The $ttl parameter can be used when purging values that have not actually changed
1032     * recently. For example, user-requested purges or cache cleanup scripts might not need
1033     * to invoke a hold-off period on cache backfills, so they can use HOLDOFF_TTL_NONE.
1034     *
1035     * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback().
1036     *
1037     * If called twice on the same key, then the last hold-off TTL takes precedence. For
1038     * idempotence, the $ttl should not vary for different delete() calls on the same key.
1039     *
1040     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1041     * @param int $ttl Tombstone TTL; Default: WANObjectCache::HOLDOFF_TTL
1042     * @return bool True if the item was purged or not found, false on failure
1043     */
1044    final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
1045        /** @noinspection PhpUnusedLocalVariableInspection */
1046        $span = $this->startOperationSpan( __FUNCTION__, $key );
1047
1048        // Purge values must be stored under the value key so that WANObjectCache::set()
1049        // can atomically merge values without accidentally undoing a recent purge and thus
1050        // violating the holdoff TTL restriction.
1051        $valueSisterKey = $this->makeSisterKey( $key, self::TYPE_VALUE );
1052
1053        if ( $ttl <= 0 ) {
1054            // A client or cache cleanup script is requesting a cache purge, so there is no
1055            // volatility period due to replica DB lag. Any recent change to an entity cached
1056            // in this key should have triggered an appropriate purge event.
1057            $ok = $this->cache->delete( $this->getRouteKey( $valueSisterKey ), $this->cache::WRITE_BACKGROUND );
1058        } else {
1059            // A cacheable entity recently changed, so there might be a volatility period due
1060            // to replica DB lag. Clients usually expect their actions to be reflected in any
1061            // of their subsequent web request. This is attainable if (a) purge relay lag is
1062            // lower than the time it takes for subsequent request by the client to arrive,
1063            // and, (b) DB replica queries have "read-your-writes" consistency due to DB lag
1064            // mitigation systems.
1065            $now = $this->getCurrentTime();
1066            // Set the key to the purge value in all datacenters
1067            $purge = self::PURGE_VAL_PREFIX . ':' . (int)$now;
1068            $ok = $this->cache->set(
1069                $this->getRouteKey( $valueSisterKey ),
1070                $purge,
1071                $ttl,
1072                $this->cache::WRITE_BACKGROUND
1073            );
1074        }
1075
1076        $keygroup = $this->determineKeyGroupForStats( $key );
1077
1078        $this->stats->getCounter( 'wanobjectcache_delete_total' )
1079            ->setLabel( 'keygroup', $keygroup )
1080            ->setLabel( 'result', ( $ok ? 'ok' : 'error' ) )
1081            ->increment();
1082
1083        return $ok;
1084    }
1085
1086    /**
1087     * Fetch the value of a timestamp "check" key
1088     *
1089     * The key will be *initialized* to the current time if not set,
1090     * so only call this method if this behavior is actually desired
1091     *
1092     * The timestamp can be used to check whether a cached value is valid.
1093     * Callers should not assume that this returns the same timestamp in
1094     * all datacenters due to relay delays.
1095     *
1096     * The level of staleness can roughly be estimated from this key, but
1097     * if the key was evicted from cache, such calculations may show the
1098     * time since expiry as ~0 seconds.
1099     *
1100     * Note that "check" keys won't collide with other regular keys.
1101     *
1102     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1103     * @return float UNIX timestamp
1104     */
1105    final public function getCheckKeyTime( $key ) {
1106        /** @noinspection PhpUnusedLocalVariableInspection */
1107        $span = $this->startOperationSpan( __FUNCTION__, $key );
1108
1109        return $this->getMultiCheckKeyTime( [ $key ] )[$key];
1110    }
1111
1112    /**
1113     * Fetch the values of each timestamp "check" key
1114     *
1115     * This works like getCheckKeyTime() except it takes a list of keys
1116     * and returns a map of timestamps instead of just that of one key
1117     *
1118     * This might be useful if both:
1119     *   - a) a class of entities each depend on hundreds of other entities
1120     *   - b) these other entities are depended upon by millions of entities
1121     *
1122     * The later entities can each use a "check" key to purge their dependee entities.
1123     * However, it is expensive for the former entities to verify against all of the relevant
1124     * "check" keys during each getWithSetCallback() call. A less expensive approach is to do
1125     * these verifications only after a "time-till-verify" (TTV) has passed. This is a middle
1126     * ground between using blind TTLs and using constant verification. The adaptiveTTL() method
1127     * can be used to dynamically adjust the TTV. Also, the initial TTV can make use of the
1128     * last-modified times of the dependent entities (either from the DB or the "check" keys).
1129     *
1130     * Example usage:
1131     * @code
1132     *     $value = $cache->getWithSetCallback(
1133     *         $cache->makeGlobalKey( 'wikibase-item', $id ),
1134     *         self::INITIAL_TTV, // initial time-till-verify
1135     *         function ( $oldValue, &$ttv, &$setOpts, $oldAsOf ) use ( $checkKeys, $cache ) {
1136     *             $now = time();
1137     *             // Use $oldValue if it passes max ultimate age and "check" key comparisons
1138     *             if ( $oldValue &&
1139     *                 $oldAsOf > max( $cache->getMultiCheckKeyTime( $checkKeys ) ) &&
1140     *                 ( $now - $oldValue['ctime'] ) <= self::MAX_CACHE_AGE
1141     *             ) {
1142     *                 // Increase time-till-verify by 50% of last time to reduce overhead
1143     *                 $ttv = $cache->adaptiveTTL( $oldAsOf, self::MAX_TTV, self::MIN_TTV, 1.5 );
1144     *                 // Unlike $oldAsOf, "ctime" is the ultimate age of the cached data
1145     *                 return $oldValue;
1146     *             }
1147     *
1148     *             $mtimes = []; // dependency last-modified times; passed by reference
1149     *             $value = [ 'data' => $this->fetchEntityData( $mtimes ), 'ctime' => $now ];
1150     *             // Guess time-till-change among the dependencies, e.g. 1/(total change rate)
1151     *             $ttc = 1 / array_sum( array_map(
1152     *                 function ( $mtime ) use ( $now ) {
1153     *                     return 1 / ( $mtime ? ( $now - $mtime ) : 900 );
1154     *                 },
1155     *                 $mtimes
1156     *             ) );
1157     *             // The time-to-verify should not be overly pessimistic nor optimistic
1158     *             $ttv = min( max( $ttc, self::MIN_TTV ), self::MAX_TTV );
1159     *
1160     *             return $value;
1161     *         },
1162     *         [ 'staleTTL' => $cache::TTL_DAY ] // keep around to verify and re-save
1163     *     );
1164     * @endcode
1165     *
1166     * @see WANObjectCache::getCheckKeyTime()
1167     * @see WANObjectCache::getWithSetCallback()
1168     *
1169     * @param string[] $keys Cache keys made with makeKey()/makeGlobalKey()
1170     * @return float[] Map of (key => UNIX timestamp)
1171     * @since 1.31
1172     */
1173    final public function getMultiCheckKeyTime( array $keys ) {
1174        /** @noinspection PhpUnusedLocalVariableInspection */
1175        $span = $this->startOperationSpan( __FUNCTION__, $keys );
1176
1177        $checkSisterKeysByKey = [];
1178        foreach ( $keys as $key ) {
1179            $checkSisterKeysByKey[$key] = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1180        }
1181
1182        $wrappedBySisterKey = $this->cache->getMulti( $checkSisterKeysByKey );
1183        $wrappedBySisterKey += array_fill_keys( $checkSisterKeysByKey, false );
1184
1185        $now = $this->getCurrentTime();
1186        $times = [];
1187        foreach ( $checkSisterKeysByKey as $key => $checkSisterKey ) {
1188            $purge = $this->parsePurgeValue( $wrappedBySisterKey[$checkSisterKey] );
1189            if ( $purge === null ) {
1190                $wrapped = $this->makeCheckPurgeValue( $now, self::HOLDOFF_TTL_NONE, $purge );
1191                $this->cache->add(
1192                    $checkSisterKey,
1193                    $wrapped,
1194                    self::CHECK_KEY_TTL,
1195                    $this->cache::WRITE_BACKGROUND
1196                );
1197            }
1198
1199            $times[$key] = $purge[self::PURGE_TIME];
1200        }
1201
1202        return $times;
1203    }
1204
1205    /**
1206     * Increase the last-purge timestamp of a "check" key in all datacenters
1207     *
1208     * This method should only be called when some heavily referenced data changes in
1209     * a significant way, such that it is impractical to call delete() on all the cache
1210     * keys that should be purged. The get*() method calls used to fetch these keys must
1211     * include the given "check" key in the relevant "check" keys argument/option.
1212     *
1213     * A "check" key essentially represents a last-modified time of an entity. When the
1214     * key is touched, the timestamp will be updated to the current time. Keys fetched
1215     * using get*() calls, that include the "check" key, will be seen as purged.
1216     *
1217     * The timestamp of the "check" key is treated as being HOLDOFF_TTL seconds in the
1218     * future by get*() methods in order to avoid race conditions where keys are updated
1219     * with stale values (e.g. from a lagged replica DB). A high TTL is set on the "check"
1220     * key, making it possible to know the timestamp of the last change to the corresponding
1221     * entities in most cases. This might use more cache space than resetCheckKey().
1222     *
1223     * When a few important keys get a large number of hits, a high cache time is usually
1224     * desired as well as "lockTSE" logic. The resetCheckKey() method is less appropriate
1225     * in such cases since the "time since expiry" cannot be inferred, causing any get()
1226     * after the reset to treat the key as being "hot", resulting in more stale value usage.
1227     *
1228     * Note that "check" keys won't collide with other regular keys.
1229     *
1230     * @see WANObjectCache::get()
1231     * @see WANObjectCache::getWithSetCallback()
1232     * @see WANObjectCache::resetCheckKey()
1233     *
1234     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1235     * @param int $holdoff HOLDOFF_TTL or HOLDOFF_TTL_NONE constant
1236     * @return bool True if the item was purged or not found, false on failure
1237     */
1238    public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
1239        /** @noinspection PhpUnusedLocalVariableInspection */
1240        $span = $this->startOperationSpan( __FUNCTION__, $key );
1241
1242        $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1243
1244        $now = $this->getCurrentTime();
1245        $purge = $this->makeCheckPurgeValue( $now, $holdoff );
1246        $ok = $this->cache->set(
1247            $this->getRouteKey( $checkSisterKey ),
1248            $purge,
1249            self::CHECK_KEY_TTL,
1250            $this->cache::WRITE_BACKGROUND
1251        );
1252
1253        $keygroup = $this->determineKeyGroupForStats( $key );
1254
1255        $this->stats->getCounter( 'wanobjectcache_check_total' )
1256            ->setLabel( 'keygroup', $keygroup )
1257            ->setLabel( 'result', ( $ok ? 'ok' : 'error' ) )
1258            ->increment();
1259
1260        return $ok;
1261    }
1262
1263    /**
1264     * Clear the last-purge timestamp of a "check" key in all datacenters
1265     *
1266     * Similar to touchCheckKey(), in that keys fetched using get*() calls, that include
1267     * the given "check" key, will be seen as purged. However, there are some differences:
1268     *   - a) The "check" key will be deleted from all caches and lazily
1269     *        re-initialized when accessed (rather than set everywhere)
1270     *   - b) Thus, dependent keys will be known to be stale, but not
1271     *        for how long (they are treated as "just" purged), which
1272     *        effects any lockTSE logic in getWithSetCallback()
1273     *   - c) Since "check" keys are initialized only on the server the key hashes
1274     *        to, any temporary ejection of that server will cause the value to be
1275     *        seen as purged as a new server will initialize the "check" key.
1276     *
1277     * The advantage over touchCheckKey() is that the "check" keys, which have high TTLs,
1278     * will only be created when a get*() method actually uses those keys. This is better
1279     * when a large number of "check" keys must be changed in a short period of time.
1280     *
1281     * Note that "check" keys won't collide with other regular keys.
1282     *
1283     * @see WANObjectCache::get()
1284     * @see WANObjectCache::getWithSetCallback()
1285     * @see WANObjectCache::touchCheckKey()
1286     *
1287     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1288     * @return bool True if the item was purged or not found, false on failure
1289     */
1290    public function resetCheckKey( $key ) {
1291        /** @noinspection PhpUnusedLocalVariableInspection */
1292        $span = $this->startOperationSpan( __FUNCTION__, $key );
1293
1294        $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_TIMESTAMP );
1295        $ok = $this->cache->delete( $this->getRouteKey( $checkSisterKey ), $this->cache::WRITE_BACKGROUND );
1296
1297        $keygroup = $this->determineKeyGroupForStats( $key );
1298
1299        $this->stats->getCounter( 'wanobjectcache_reset_total' )
1300            ->setLabel( 'keygroup', $keygroup )
1301            ->setLabel( 'result', ( $ok ? 'ok' : 'error' ) )
1302            ->increment();
1303
1304        return $ok;
1305    }
1306
1307    /**
1308     * Method to fetch/regenerate a cache key
1309     *
1310     * On cache miss, the key will be set to the callback result via set()
1311     * (unless the callback returns false) and that result will be returned.
1312     * The arguments supplied to the callback are:
1313     *   - $oldValue: prior cache value or false if none was present
1314     *   - &$ttl: alterable reference to the TTL to be assigned to the new value
1315     *   - &$setOpts: alterable reference to the set() options to be used with the new value
1316     *   - $oldAsOf: generation UNIX timestamp of $oldValue or null if not present (since 1.28)
1317     *   - $params: custom field/value map as defined by $cbParams (since 1.35)
1318     *
1319     * It is strongly recommended to set the 'lag' and 'since' fields to avoid race conditions
1320     * that can cause stale values to get stuck at keys. Usually, callbacks ignore the current
1321     * value, but it can be used to maintain "most recent X" values that come from time or
1322     * sequence based source data, provided that the "as of" id/time is tracked. Note that
1323     * preemptive regeneration and $checkKeys can result in a non-false current value.
1324     *
1325     * Usage of $checkKeys is similar to get() and getMulti(). However, rather than the caller
1326     * having to inspect a "current time left" variable (e.g. $curTTL, $curTTLs), a cache
1327     * regeneration will automatically be triggered using the callback.
1328     *
1329     * The $ttl argument and "hotTTR" option (in $opts) use time-dependent randomization
1330     * to avoid stampedes. Keys that are slow to regenerate and either heavily used
1331     * or subject to explicit (unpredictable) purges, may need additional mechanisms.
1332     * The simplest way to avoid stampedes for such keys is to use 'lockTSE' (in $opts).
1333     * If explicit purges are needed, also:
1334     *   - a) Pass $key into $checkKeys
1335     *   - b) Use touchCheckKey( $key ) instead of delete( $key )
1336     *
1337     * This applies cache server I/O stampede protection against duplicate cache sets.
1338     * This is important when the callback is slow and/or yields large values for a key.
1339     *
1340     * Example usage (typical key):
1341     * @code
1342     *     $catInfo = $cache->getWithSetCallback(
1343     *         // Key to store the cached value under
1344     *         $cache->makeKey( 'cat-attributes', $catId ),
1345     *         // Time-to-live (in seconds)
1346     *         $cache::TTL_MINUTE,
1347     *         // Function that derives the new key value
1348     *         function ( $oldValue, &$ttl, array &$setOpts ) {
1349     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1350     *             // Account for any snapshot/replica DB lag
1351     *             $setOpts += Database::getCacheSetOptions( $dbr );
1352     *
1353     *             return $dbr->selectRow( ... );
1354     *        }
1355     *     );
1356     * @endcode
1357     *
1358     * Example usage (key that is expensive and hot):
1359     * @code
1360     *     $catConfig = $cache->getWithSetCallback(
1361     *         // Key to store the cached value under
1362     *         $cache->makeKey( 'site-cat-config' ),
1363     *         // Time-to-live (in seconds)
1364     *         $cache::TTL_DAY,
1365     *         // Function that derives the new key value
1366     *         function ( $oldValue, &$ttl, array &$setOpts ) {
1367     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1368     *             // Account for any snapshot/replica DB lag
1369     *             $setOpts += Database::getCacheSetOptions( $dbr );
1370     *
1371     *             return CatConfig::newFromRow( $dbr->selectRow( ... ) );
1372     *         },
1373     *         [
1374     *             // Calling touchCheckKey() on this key purges the cache
1375     *             'checkKeys' => [ $cache->makeKey( 'site-cat-config' ) ],
1376     *             // Try to only let one datacenter thread manage cache updates at a time
1377     *             'lockTSE' => 30,
1378     *             // Avoid querying cache servers multiple times in a web request
1379     *             'pcTTL' => $cache::TTL_PROC_LONG
1380     *         ]
1381     *     );
1382     * @endcode
1383     *
1384     * Example usage (key with dynamic dependencies):
1385     * @code
1386     *     $catState = $cache->getWithSetCallback(
1387     *         // Key to store the cached value under
1388     *         $cache->makeKey( 'cat-state', $cat->getId() ),
1389     *         // Time-to-live (seconds)
1390     *         $cache::TTL_HOUR,
1391     *         // Function that derives the new key value
1392     *         function ( $oldValue, &$ttl, array &$setOpts ) {
1393     *             // Determine new value from the DB
1394     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1395     *             // Account for any snapshot/replica DB lag
1396     *             $setOpts += Database::getCacheSetOptions( $dbr );
1397     *
1398     *             return CatState::newFromResults( $dbr->select( ... ) );
1399     *         },
1400     *         [
1401     *              // The "check" keys that represent things the value depends on;
1402     *              // Calling touchCheckKey() on any of them purges the cache
1403     *             'checkKeys' => [
1404     *                 $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ),
1405     *                 $cache->makeKey( 'people-present', $cat->getHouseId() ),
1406     *                 $cache->makeKey( 'cat-laws', $cat->getCityId() ),
1407     *             ]
1408     *         ]
1409     *     );
1410     * @endcode
1411     *
1412     * Example usage (key that is expensive with too many DB dependencies for "check" keys):
1413     * @code
1414     *     $catToys = $cache->getWithSetCallback(
1415     *         // Key to store the cached value under
1416     *         $cache->makeKey( 'cat-toys', $catId ),
1417     *         // Time-to-live (seconds)
1418     *         $cache::TTL_HOUR,
1419     *         // Function that derives the new key value
1420     *         function ( $oldValue, &$ttl, array &$setOpts ) {
1421     *             // Determine new value from the DB
1422     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1423     *             // Account for any snapshot/replica DB lag
1424     *             $setOpts += Database::getCacheSetOptions( $dbr );
1425     *
1426     *             return CatToys::newFromResults( $dbr->select( ... ) );
1427     *         },
1428     *         [
1429     *              // Get the highest timestamp of any of the cat's toys
1430     *             'touchedCallback' => function ( $value ) use ( $catId ) {
1431     *                 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1432     *                 $ts = $dbr->selectField( 'cat_toys', 'MAX(ct_touched)', ... );
1433     *
1434     *                 return wfTimestampOrNull( TS::UNIX, $ts );
1435     *             },
1436     *             // Avoid DB queries for repeated access
1437     *             'pcTTL' => $cache::TTL_PROC_SHORT
1438     *         ]
1439     *     );
1440     * @endcode
1441     *
1442     * Example usage (hot key holding most recent 100 events):
1443     * @code
1444     *     $lastCatActions = $cache->getWithSetCallback(
1445     *         // Key to store the cached value under
1446     *         $cache->makeKey( 'cat-last-actions', 100 ),
1447     *         // Time-to-live (in seconds)
1448     *         10,
1449     *         // Function that derives the new key value
1450     *         function ( $oldValue, &$ttl, array &$setOpts ) {
1451     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1452     *             // Account for any snapshot/replica DB lag
1453     *             $setOpts += Database::getCacheSetOptions( $dbr );
1454     *
1455     *             // Start off with the last cached list
1456     *             $list = $oldValue ?: [];
1457     *             // Fetch the last 100 relevant rows in descending order;
1458     *             // only fetch rows newer than $list[0] to reduce scanning
1459     *             $rows = iterator_to_array( $dbr->select( ... ) );
1460     *             // Merge them and get the new "last 100" rows
1461     *             return array_slice( array_merge( $new, $list ), 0, 100 );
1462     *        },
1463     *        [
1464     *             // Try to only let one datacenter thread manage cache updates at a time
1465     *             'lockTSE' => 30,
1466     *             // Use a magic value when no cache value is ready rather than stampeding
1467     *             'busyValue' => 'computing'
1468     *        ]
1469     *     );
1470     * @endcode
1471     *
1472     * Example usage (key holding an LRU subkey:value map; this can avoid flooding cache with
1473     * keys for an unlimited set of (constraint,situation) pairs, thereby avoiding elevated
1474     * cache evictions and wasted memory):
1475     * @code
1476     *     $catSituationTolerabilityCache = $this->cache->getWithSetCallback(
1477     *         // Group by constraint ID/hash, cat family ID/hash, or something else useful
1478     *         $this->cache->makeKey( 'cat-situation-tolerability-checks', $groupKey ),
1479     *         WANObjectCache::TTL_DAY, // rarely used groups should fade away
1480     *         // The $scenarioKey format is $constraintId:<ID/hash of $situation>
1481     *         function ( $cacheMap ) use ( $scenarioKey, $constraintId, $situation ) {
1482     *             $lruCache = MapCacheLRU::newFromArray( $cacheMap ?: [], self::CACHE_SIZE );
1483     *             $result = $lruCache->get( $scenarioKey ); // triggers LRU bump if present
1484     *             if ( $result === null || $this->isScenarioResultExpired( $result ) ) {
1485     *                 $result = $this->checkScenarioTolerability( $constraintId, $situation );
1486     *                 $lruCache->set( $scenarioKey, $result, 3 / 8 );
1487     *             }
1488     *             // Save the new LRU cache map and reset the map's TTL
1489     *             return $lruCache->toArray();
1490     *         },
1491     *         [
1492     *             // Once map is > 1 sec old, consider refreshing
1493     *             'ageNew' => 1,
1494     *             // Update within 5 seconds after "ageNew" given a 1hz cache check rate
1495     *             'hotTTR' => 5,
1496     *             // Avoid querying cache servers multiple times in a request; this also means
1497     *             // that a request can only alter the value of any given constraint key once
1498     *             'pcTTL' => WANObjectCache::TTL_PROC_LONG
1499     *         ]
1500     *     );
1501     *     $tolerability = isset( $catSituationTolerabilityCache[$scenarioKey] )
1502     *         ? $catSituationTolerabilityCache[$scenarioKey]
1503     *         : $this->checkScenarioTolerability( $constraintId, $situation );
1504     * @endcode
1505     *
1506     * @see WANObjectCache::get()
1507     * @see WANObjectCache::set()
1508     *
1509     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1510     * @param int $ttl Nominal seconds-to-live for newly computed values. Special values are:
1511     *   - WANObjectCache::TTL_INDEFINITE: Cache forever (subject to LRU-style evictions)
1512     *   - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
1513     * @param callable $callback Value generation function
1514     * @param array $opts Options map:
1515     *   - checkKeys: List of "check" keys. The key at $key will be seen as stale when either
1516     *      touchCheckKey() or resetCheckKey() is called on any of the keys in this list. This
1517     *      is useful if thousands or millions of keys depend on the same entity. The entity can
1518     *      simply have its "check" key updated whenever the entity is modified.
1519     *      Default: [].
1520     *   - graceTTL: If the key is stale due to a purge (by "checkKeys" or "touchedCallback")
1521     *      less than this many seconds ago, consider reusing the stale value. The odds of a
1522     *      refresh become more likely over time, becoming certain once the grace period is
1523     *      reached. This can reduce traffic spikes when millions of keys are compared to the
1524     *      same  "check" key and touchCheckKey() or resetCheckKey() is called on that "check" key.
1525     *      This option is not useful for avoiding traffic spikes in the case of the key simply
1526     *      expiring on account of its TTL (use "lowTTL" instead).
1527     *      Default: WANObjectCache::GRACE_TTL_NONE.
1528     *   - lockTSE: If the value is stale and the "time since expiry" (TSE) is less than the given
1529     *      number of seconds ago, then reuse the stale value if another such thread is already
1530     *      regenerating the value. The TSE of the key is influenced by purges (e.g. via delete(),
1531     *      "checkKeys", "touchedCallback"), and various other options (e.g. "staleTTL"). A low
1532     *      enough TSE is assumed to indicate a high enough key access rate to justify stampede
1533     *      avoidance. Note that no cache value exists after deletion, expiration, or eviction
1534     *      at the storage-layer; to prevent stampedes during these cases, use "busyValue".
1535     *      Default: WANObjectCache::TSE_NONE.
1536     *   - busyValue: Specify a placeholder value to use when no value exists and another thread
1537     *      is currently regenerating it. This assures that cache stampedes cannot happen if the
1538     *      value falls out of cache. This also mitigates stampedes when value regeneration
1539     *      becomes very slow (greater than $ttl/"lowTTL"). If this is a closure, then it will
1540     *      be invoked to get the placeholder when needed.
1541     *      Default: null.
1542     *   - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids
1543     *      network I/O when a key is read several times. This will not cache when the callback
1544     *      returns false, however. Note that any purges will not be seen while process cached;
1545     *      since the callback should use replica DBs and they may be lagged or have snapshot
1546     *      isolation anyway, this should not typically matter.
1547     *      Default: WANObjectCache::TTL_UNCACHEABLE.
1548     *   - pcGroup: Process cache group to use instead of the primary one. If set, this must be
1549     *      of the format ALPHANUMERIC_NAME:MAX_KEY_SIZE, e.g. "mydata:10". Use this for storing
1550     *      large values, small yet numerous values, or some values with a high cost of eviction.
1551     *      It is generally preferable to use a class constant when setting this value.
1552     *      This has no effect unless pcTTL is used.
1553     *      Default: WANObjectCache::PC_PRIMARY.
1554     *   - version: Integer version number. This lets callers make breaking changes to the format
1555     *      of cached values without causing problems for sites that use non-instantaneous code
1556     *      deployments. Old and new code will recognize incompatible versions and purges from
1557     *      both old and new code will been seen by each other. When this method encounters an
1558     *      incompatibly versioned value at the provided key, a "variant key" will be used for
1559     *      reading from and saving to cache. The variant key is specific to the key and version
1560     *      number provided to this method. If the variant key value is older than that of the
1561     *      provided key, or the provided key is non-existant, then the variant key will be seen
1562     *      as non-existant. Therefore, delete() calls purge the provided key's variant keys.
1563     *      The "checkKeys" and "touchedCallback" options still apply to variant keys as usual.
1564     *      Avoid storing class objects, as this reduces compatibility (due to serialization).
1565     *      Default: null.
1566     *   - minAsOf: Reject values if they were generated before this UNIX timestamp.
1567     *      This is useful if the source of a key is suspected of having possibly changed
1568     *      recently, and the caller wants any such changes to be reflected.
1569     *      Default: WANObjectCache::MIN_TIMESTAMP_NONE.
1570     *   - lowTTL: Consider pre-emptive updates once the current TTL (seconds) of the key is less
1571     *      than this. It becomes more likely over time, and is more likely when a key is popular,
1572     *      becoming certain as the key approaches expiry. This can avoid cache stampedes by
1573     *      regenerating your value on a random request before the key expires, instead of letting
1574     *      it expire and all requests having to regen together.
1575     *      This feature is enabled by default (LOW_TTL=60s) but should have little to no impact
1576     *      on most keys, while automatically avoiding stampedes on hot keys. Set to 0 to disable.
1577     *      See also: WANObjectCache::worthRefreshExpiring.
1578     *      Default: WANObjectCache::LOW_TTL.
1579     *   - hotTTR: Schedule pre-emptive updates on popular keys once every $hotTTR seconds.
1580     *      If one of your keys becomes popular (more than 1 req/s), we schedule an async refresh on
1581     *      a random request roughly once every $hotTTR seconds. For keys with an even higher rate,
1582     *      this will happen sooner. During the first few seconds after a value is generated,
1583     *      this option is ignored as controlled by the "ageNew" option. This feature is enabled by
1584     *      default to avoid stale data on heavily referenced keys (e.g. due to lost purges),
1585     *      and should only impact keys that are hot. Set to 0 to disable this feature.
1586     *      See also: WANObjectCache::worthRefreshPopular.
1587     *      Default: WANObjectCache::HOT_TTR.
1588     *   - ageNew: Start pre-emptive "hotTTR" updates after a key reaches this age in seconds.
1589     *      Set `hotTTR: 0` to disable this feature. Setting ageNew to zero does not disable
1590     *      the hotTTR feature.
1591     *      Default: WANObjectCache::AGE_NEW.
1592     *   - staleTTL: Seconds to keep the key around if it is stale. This means that on cache
1593     *      miss the callback may get $oldValue/$oldAsOf values for keys that have already been
1594     *      expired for this specified time. This is useful if adaptiveTTL() is used on the old
1595     *      value's as-of time when it is verified as still being correct.
1596     *      Default: WANObjectCache::STALE_TTL_NONE
1597     *   - touchedCallback: A callback that takes the current value and returns a UNIX timestamp
1598     *      indicating the last time a dynamic dependency changed. Null can be returned if there
1599     *      are no relevant dependency changes to check. This can be used to check against things
1600     *      like last-modified times of files or DB timestamp fields. This should generally not be
1601     *      used for small and easily queried values in a DB if the callback itself ends up doing
1602     *      a similarly expensive DB query to check a timestamp. Usages of this option makes the
1603     *      most sense for values that are moderately to highly expensive to regenerate and easy
1604     *      to query for dependency timestamps. The use of "pcTTL" reduces timestamp queries.
1605     *      Default: null.
1606     *   - segmentable: Allow partitioning of the value if it is a large string. Default: false.
1607     *
1608     * @param array $cbParams Custom field/value map to pass to the callback (since 1.35)
1609     * @phpcs:ignore Generic.Files.LineLength
1610     * @phan-param array{checkKeys?:string[],graceTTL?:int,lockTSE?:int,busyValue?:mixed,pcTTL?:int,pcGroup?:string,version?:int,minAsOf?:float|int,hotTTR?:int,lowTTL?:int,ageNew?:int,staleTTL?:int,touchedCallback?:callable,segmentable?:bool} $opts
1611     * @return mixed Value found or written to the key
1612     * @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf
1613     * @note Options added in 1.31: staleTTL, graceTTL
1614     * @note Options added in 1.33: touchedCallback
1615     * @note Callable type hints are not used to avoid class-autoloading
1616     */
1617    final public function getWithSetCallback(
1618        $key, $ttl, $callback, array $opts = [], array $cbParams = []
1619    ) {
1620        /** @noinspection PhpUnusedLocalVariableInspection */
1621        $span = $this->startOperationSpan( __FUNCTION__, $key );
1622
1623        $version = $opts['version'] ?? null;
1624        $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
1625        $pCache = ( $pcTTL >= 0 )
1626            ? $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY )
1627            : null;
1628
1629        // Use the process cache if requested as long as no outer cache callback is running.
1630        // Nested callback process cache use is not lag-safe with regard to HOLDOFF_TTL since
1631        // process cached values are more lagged than persistent ones as they are not purged.
1632        if ( $pCache && $this->callbackDepth == 0 ) {
1633            $cached = $pCache->get( $key, $pcTTL, false );
1634            if ( $cached !== false ) {
1635                $this->logger->debug( "getWithSetCallback($key): process cache hit" );
1636                return $cached;
1637            }
1638        }
1639
1640        [ $value, $valueVersion, $curAsOf ] = $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
1641        if ( $valueVersion !== $version ) {
1642            // Current value has a different version; use the variant key for this version.
1643            // Regenerate the variant value if it is not newer than the main value at $key
1644            // so that purges to the main key propagate to the variant value.
1645            $this->logger->debug( "getWithSetCallback($key): using variant key" );
1646            [ $value ] = $this->fetchOrRegenerate(
1647                $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), (string)$version ),
1648                $ttl,
1649                $callback,
1650                [ 'version' => null, 'minAsOf' => $curAsOf ] + $opts,
1651                $cbParams
1652            );
1653        }
1654
1655        // Update the process cache if enabled
1656        if ( $pCache && $value !== false ) {
1657            $pCache->set( $key, $value );
1658        }
1659
1660        return $value;
1661    }
1662
1663    /**
1664     * Do the actual I/O for getWithSetCallback() when needed
1665     *
1666     * @see WANObjectCache::getWithSetCallback()
1667     *
1668     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1669     * @param int $ttl
1670     * @param callable $callback
1671     * @param array $opts
1672     * @param array $cbParams
1673     * @return array Ordered list of the following:
1674     *   - Cached or regenerated value
1675     *   - Cached or regenerated value version number or null if not versioned
1676     *   - Timestamp of the current cached value at the key or null if there is no value
1677     * @note Callable type hints are not used to avoid class-autoloading
1678     */
1679    private function fetchOrRegenerate( $key, $ttl, $callback, array $opts, array $cbParams ) {
1680        $checkKeys = $opts['checkKeys'] ?? [];
1681        $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
1682        $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
1683        $hotTTR = $opts['hotTTR'] ?? self::HOT_TTR;
1684        $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
1685        $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
1686        $touchedCb = $opts['touchedCallback'] ?? null;
1687        $startTime = $this->getCurrentTime();
1688
1689        $keygroup = $this->determineKeyGroupForStats( $key );
1690
1691        // Get the current key value and its metadata
1692        $curState = $this->fetchKeys( [ $key ], $checkKeys, $startTime, $touchedCb )[$key];
1693        $curValue = $curState[self::RES_VALUE];
1694
1695        // Use the cached value if it exists and is not due for synchronous regeneration
1696        if ( $this->isAcceptablyFreshValue( $curState, $graceTTL, $minAsOf ) ) {
1697            if ( !$this->isLotteryRefreshDue( $curState, $lowTTL, $ageNew, $hotTTR, $startTime ) ) {
1698                $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1699                    ->setLabel( 'keygroup', $keygroup )
1700                    ->setLabel( 'result', 'hit' )
1701                    ->setLabel( 'reason', 'good' )
1702                    ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1703
1704                return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1705            } elseif ( $this->scheduleAsyncRefresh( $key, $ttl, $callback, $opts, $cbParams ) ) {
1706                $this->logger->debug( "fetchOrRegenerate($key): hit with async refresh" );
1707
1708                $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1709                    ->setLabel( 'keygroup', $keygroup )
1710                    ->setLabel( 'result', 'hit' )
1711                    ->setLabel( 'reason', 'refresh' )
1712                    ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1713
1714                return [ $curValue, $curState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1715            } else {
1716                $this->logger->debug( "fetchOrRegenerate($key): hit with sync refresh" );
1717            }
1718        }
1719
1720        $isKeyTombstoned = ( $curState[self::RES_TOMB_AS_OF] !== null );
1721        // Use the interim key as a temporary alternative if the key is tombstoned
1722        if ( $isKeyTombstoned ) {
1723            $volState = $this->getInterimValue( $key, $minAsOf, $startTime, $touchedCb );
1724            $volValue = $volState[self::RES_VALUE];
1725        } else {
1726            $volState = $curState;
1727            $volValue = $curValue;
1728        }
1729
1730        // During the volatile "hold-off" period that follows a purge of the key, the value
1731        // will be regenerated many times if frequently accessed. This is done to mitigate
1732        // the effects of backend replication lag as soon as possible. However, throttle the
1733        // overhead of locking and regeneration by reusing values recently written to cache
1734        // tens of milliseconds ago. Verify the "as of" time against the last purge event.
1735        $lastPurgeTime = max(
1736            // RES_TOUCH_AS_OF depends on the value (possibly from the interim key)
1737            $volState[self::RES_TOUCH_AS_OF],
1738            $curState[self::RES_TOMB_AS_OF],
1739            $curState[self::RES_CHECK_AS_OF]
1740        );
1741        $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSITIVE );
1742
1743        if ( $volState[self::RES_VALUE] === false || $volState[self::RES_AS_OF] < $safeMinAsOf ) {
1744            $isExtremelyNewValue = false;
1745        } else {
1746            $age = $startTime - $volState[self::RES_AS_OF];
1747            $isExtremelyNewValue = ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 );
1748        }
1749        if ( $isExtremelyNewValue ) {
1750            $this->logger->debug( "fetchOrRegenerate($key): volatile hit" );
1751
1752            $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1753                ->setLabel( 'keygroup', $keygroup )
1754                ->setLabel( 'result', 'hit' )
1755                ->setLabel( 'reason', 'volatile' )
1756                ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1757
1758            return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1759        }
1760
1761        $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
1762        $busyValue = $opts['busyValue'] ?? null;
1763        $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
1764        $segmentable = $opts['segmentable'] ?? false;
1765        $version = $opts['version'] ?? null;
1766
1767        // Determine whether one thread per datacenter should handle regeneration at a time
1768        $useRegenerationLock =
1769            // Note that since tombstones no-op set(), $lockTSE and $curTTL cannot be used to
1770            // deduce the key hotness because |$curTTL| will always keep increasing until the
1771            // tombstone expires or is overwritten by a new tombstone. Also, even if $lockTSE
1772            // is not set, constant regeneration of a key for the tombstone lifetime might be
1773            // very expensive. Assume tombstoned keys are possibly hot in order to reduce
1774            // the risk of high regeneration load after the delete() method is called.
1775            $isKeyTombstoned ||
1776            // Assume a key is hot if requested soon ($lockTSE seconds) after purge.
1777            // This avoids stampedes when timestamps from $checkKeys/$touchedCb bump.
1778            (
1779                $curState[self::RES_CUR_TTL] !== null &&
1780                $curState[self::RES_CUR_TTL] <= 0 &&
1781                abs( $curState[self::RES_CUR_TTL] ) <= $lockTSE
1782            ) ||
1783            // Assume a key is hot if there is no value and a busy fallback is given.
1784            // This avoids stampedes on eviction or preemptive regeneration taking too long.
1785            ( $busyValue !== null && $volValue === false );
1786
1787        // If a regeneration lock is required, threads that do not get the lock will try to use
1788        // the stale value, the interim value, or the $busyValue placeholder, in that order. If
1789        // none of those are set then all threads will bypass the lock and regenerate the value.
1790        $mutexKey = $this->makeSisterKey( $key, self::TYPE_MUTEX );
1791        // Note that locking is not bypassed due to I/O errors; this avoids stampedes
1792        $hasLock = $useRegenerationLock && $this->cache->add( $mutexKey, 1, self::LOCK_TTL );
1793        if ( $useRegenerationLock && !$hasLock ) {
1794            // Determine if there is stale or volatile cached value that is still usable
1795            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
1796            if ( $this->isValid( $volValue, $volState[self::RES_AS_OF], $minAsOf ) ) {
1797                $this->logger->debug( "fetchOrRegenerate($key): returning stale value" );
1798
1799                $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1800                    ->setLabel( 'keygroup', $keygroup )
1801                    ->setLabel( 'result', 'hit' )
1802                    ->setLabel( 'reason', 'stale' )
1803                    ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1804
1805                return [ $volValue, $volState[self::RES_VERSION], $curState[self::RES_AS_OF] ];
1806            } elseif ( $busyValue !== null ) {
1807                $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1808                $this->logger->debug( "fetchOrRegenerate($key): busy $miss" );
1809
1810                $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1811                    ->setLabel( 'keygroup', $keygroup )
1812                    ->setLabel( 'result', $miss )
1813                    ->setLabel( 'reason', 'busy' )
1814                    ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1815
1816                $placeholderValue = ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
1817
1818                return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ];
1819            }
1820        }
1821
1822        // Generate the new value given any prior value with a matching version
1823        $setOpts = [];
1824        $preCallbackTime = $this->getCurrentTime();
1825        ++$this->callbackDepth;
1826        // https://github.com/phan/phan/issues/4419
1827        /** @noinspection PhpUnusedLocalVariableInspection */
1828        $value = null;
1829        try {
1830            $value = $callback(
1831                ( $curState[self::RES_VERSION] === $version ) ? $curValue : false,
1832                $ttl,
1833                $setOpts,
1834                ( $curState[self::RES_VERSION] === $version ) ? $curState[self::RES_AS_OF] : null,
1835                $cbParams
1836            );
1837        } finally {
1838            --$this->callbackDepth;
1839        }
1840        $postCallbackTime = $this->getCurrentTime();
1841
1842        // How long it took to generate the value
1843        $walltime = max( $postCallbackTime - $preCallbackTime, 0.0 );
1844
1845        $this->stats->getTiming( 'wanobjectcache_regen_seconds' )
1846            ->setLabel( 'keygroup', $keygroup )
1847            ->observe( 1e3 * $walltime );
1848
1849        // Attempt to save the newly generated value if applicable
1850        if (
1851            // Callback yielded a cacheable value
1852            ( $value !== false && $ttl >= 0 ) &&
1853            // Current thread was not raced out of a regeneration lock or key is tombstoned
1854            ( !$useRegenerationLock || $hasLock || $isKeyTombstoned )
1855        ) {
1856            // If the key is write-holed then use the (volatile) interim key as an alternative
1857            if ( $isKeyTombstoned ) {
1858                $this->setInterimValue(
1859                    $key,
1860                    $value,
1861                    $lockTSE,
1862                    $version,
1863                    $segmentable
1864                );
1865            } else {
1866                $this->setMainValue(
1867                    $key,
1868                    $value,
1869                    $ttl,
1870                    $version,
1871                    $walltime,
1872                    // @phan-suppress-next-line PhanCoalescingAlwaysNull
1873                    $setOpts['lag'] ?? 0,
1874                    // @phan-suppress-next-line PhanCoalescingAlwaysNull
1875                    $setOpts['since'] ?? $preCallbackTime,
1876                    // @phan-suppress-next-line PhanCoalescingAlwaysNull
1877                    $setOpts['pending'] ?? false,
1878                    $lockTSE,
1879                    $staleTTL,
1880                    $segmentable,
1881                    ( $curValue === false )
1882                );
1883            }
1884        }
1885
1886        if ( $hasLock ) {
1887            $this->cache->delete( $mutexKey, $this->cache::WRITE_BACKGROUND );
1888        }
1889
1890        $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
1891        $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" );
1892
1893        $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' )
1894            ->setLabel( 'keygroup', $keygroup )
1895            ->setLabel( 'result', $miss )
1896            ->setLabel( 'reason', 'compute' )
1897            ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) );
1898
1899        return [ $value, $version, $curState[self::RES_AS_OF] ];
1900    }
1901
1902    /**
1903     * Get a sister key that should be collocated with a base cache key
1904     *
1905     * The keys will bear the WANCache prefix and use the configured coalescing scheme
1906     *
1907     * @param string $baseKey Cache key made with makeKey()/makeGlobalKey()
1908     * @param string $typeChar Consistent hashing agnostic suffix character matching [a-zA-Z]
1909     * @return string Sister key
1910     */
1911    private function makeSisterKey( string $baseKey, string $typeChar ) {
1912        if ( $this->coalesceScheme === self::SCHEME_HASH_STOP ) {
1913            // Key style: "WANCache:<base key>|#|<character>"
1914            $sisterKey = 'WANCache:' . $baseKey . '|#|' . $typeChar;
1915        } else {
1916            // Key style: "WANCache:{<base key>}:<character>"
1917            $sisterKey = 'WANCache:{' . $baseKey . '}:' . $typeChar;
1918        }
1919        return $sisterKey;
1920    }
1921
1922    /**
1923     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1924     * @param float $minAsOf Minimum acceptable value "as of" UNIX timestamp
1925     * @param float $now Fetch time to determine "age" metadata
1926     * @param callable|null $touchedCb Function to find the max "dependency touched" UNIX timestamp
1927     * @return array<int,mixed> Result map/n-tuple from unwrap()
1928     * @phan-return array{0:mixed,1:mixed,2:?float,3:?int,4:?float,5:?float,6:?float,7:?float}
1929     * @note Callable type hints are not used to avoid class-autoloading
1930     */
1931    private function getInterimValue( $key, $minAsOf, $now, $touchedCb ) {
1932        if ( $this->useInterimHoldOffCaching ) {
1933            $interimSisterKey = $this->makeSisterKey( $key, self::TYPE_INTERIM );
1934            $wrapped = $this->cache->get( $interimSisterKey );
1935            $res = $this->unwrap( $wrapped, $now );
1936            if ( $res[self::RES_VALUE] !== false && $res[self::RES_AS_OF] >= $minAsOf ) {
1937                if ( $touchedCb !== null ) {
1938                    // Update "last purge time" since the $touchedCb timestamp depends on $value
1939                    // Get the new "touched timestamp", accounting for callback-checked dependencies
1940                    $res[self::RES_TOUCH_AS_OF] = max(
1941                        $touchedCb( $res[self::RES_VALUE] ),
1942                        $res[self::RES_TOUCH_AS_OF]
1943                    );
1944                }
1945
1946                return $res;
1947            }
1948        }
1949
1950        return $this->unwrap( false, $now );
1951    }
1952
1953    /**
1954     * @param string $key Cache key made with makeKey()/makeGlobalKey()
1955     * @param mixed $value
1956     * @param int|float $ttl
1957     * @param int|null $version Value version number
1958     * @param bool $segmentable
1959     * @return bool Success
1960     */
1961    private function setInterimValue(
1962        $key,
1963        $value,
1964        $ttl,
1965        ?int $version,
1966        bool $segmentable
1967    ) {
1968        $now = $this->getCurrentTime();
1969        $ttl = max( self::INTERIM_KEY_TTL, (int)$ttl );
1970
1971        // Wrap that value with time/TTL/version metadata
1972        $wrapped = $this->wrap( $value, $ttl, $version, $now );
1973
1974        $flags = $this->cache::WRITE_BACKGROUND;
1975        if ( $segmentable ) {
1976            $flags |= $this->cache::WRITE_ALLOW_SEGMENTS;
1977        }
1978
1979        return $this->cache->set(
1980            $this->makeSisterKey( $key, self::TYPE_INTERIM ),
1981            $wrapped,
1982            $ttl,
1983            $flags
1984        );
1985    }
1986
1987    /**
1988     * Method to fetch multiple cache keys at once with regeneration
1989     *
1990     * This works the same as getWithSetCallback() except:
1991     *   - a) The $keys argument must be the result of WANObjectCache::makeMultiKeys()
1992     *   - b) The $callback argument expects a function that returns an entity value, using
1993     *        boolean "false" if it does not exist. The callback takes the following arguments:
1994     *        - $id: ID of the entity to query
1995     *        - $oldValue: prior cache value or false if none was present
1996     *        - &$ttl: reference to the TTL to be assigned to the new value (alterable)
1997     *        - &$setOpts: reference to the new value set() options (alterable)
1998     *        - $oldAsOf: generation UNIX timestamp of $oldValue or null if not present
1999     *   - c) The return value is a map of (cache key => value) in the order of $keyedIds
2000     *
2001     * @see WANObjectCache::getWithSetCallback()
2002     * @see WANObjectCache::getMultiWithUnionSetCallback()
2003     *
2004     * Example usage:
2005     * @code
2006     *     $rows = $cache->getMultiWithSetCallback(
2007     *         // Map of cache keys to entity IDs
2008     *         $cache->makeMultiKeys(
2009     *             $this->fileVersionIds(),
2010     *             function ( $id, $cache ) {
2011     *                 return $cache->makeKey( 'file-version', $id );
2012     *             }
2013     *         ),
2014     *         // Time-to-live (in seconds)
2015     *         $cache::TTL_DAY,
2016     *         // Function that derives the new key value
2017     *         function ( $id, $oldValue, &$ttl, array &$setOpts ) {
2018     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
2019     *             // Account for any snapshot/replica DB lag
2020     *             $setOpts += Database::getCacheSetOptions( $dbr );
2021     *
2022     *             // Load the row for this file
2023     *             $queryInfo = File::getQueryInfo();
2024     *             $row = $dbr->selectRow(
2025     *                 $queryInfo['tables'],
2026     *                 $queryInfo['fields'],
2027     *                 [ 'id' => $id ],
2028     *                 __METHOD__,
2029     *                 [],
2030     *                 $queryInfo['joins']
2031     *             );
2032     *
2033     *             return $row ? (array)$row : false;
2034     *         },
2035     *         [
2036     *             // Process cache for 30 seconds
2037     *             'pcTTL' => 30,
2038     *             // Use a dedicated 500 item cache (initialized on-the-fly)
2039     *             'pcGroup' => 'file-versions:500'
2040     *         ]
2041     *     );
2042     *     $files = array_map( self::newFromRow( ... ), $rows );
2043     * @endcode
2044     *
2045     * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
2046     * @param int $ttl Seconds to live for key updates
2047     * @param callable $callback Callback that yields entity generation callbacks
2048     * @param array $opts Options map similar to that of getWithSetCallback()
2049     * @return mixed[] Map of (cache key => value) in the same order as $keyedIds
2050     * @since 1.28
2051     */
2052    final public function getMultiWithSetCallback(
2053        ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2054    ) {
2055        $span = $this->startOperationSpan( __FUNCTION__, '' );
2056        if ( $span->getContext()->isSampled() ) {
2057            $span->setAttributes( [
2058                'org.wikimedia.wancache.multi_count' => $keyedIds->count(),
2059                'org.wikimedia.wancache.ttl' => $ttl,
2060            ] );
2061        }
2062        // Batch load required keys into the in-process warmup cache
2063        $this->warmupCache = $this->fetchWrappedValuesForWarmupCache(
2064            $this->getNonProcessCachedMultiKeys( $keyedIds, $opts ),
2065            $opts['checkKeys'] ?? []
2066        );
2067        $this->warmupKeyMisses = 0;
2068
2069        // The required callback signature includes $id as the first argument for convenience
2070        // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2071        // callback with a proxy callback that has the standard getWithSetCallback() signature.
2072        // This is defined only once per batch to avoid closure creation overhead.
2073        $proxyCb = static function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2074            use ( $callback )
2075        {
2076            return $callback( $params['id'], $oldValue, $ttl, $setOpts, $oldAsOf );
2077        };
2078
2079        // Get the order-preserved result map using the warm-up cache
2080        $values = [];
2081        foreach ( $keyedIds as $key => $id ) {
2082            $values[$key] = $this->getWithSetCallback(
2083                $key,
2084                $ttl,
2085                $proxyCb,
2086                $opts,
2087                [ 'id' => $id ]
2088            );
2089        }
2090
2091        $this->warmupCache = [];
2092
2093        return $values;
2094    }
2095
2096    /**
2097     * Method to fetch/regenerate multiple cache keys at once
2098     *
2099     * This works the same as getWithSetCallback() except:
2100     *   - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
2101     *   - b) The $callback argument expects a function that returns a map of (ID => new value),
2102     *        using boolean "false" for entities that could not be found, for all entity IDs in
2103     *        $ids. The callback takes the following arguments:
2104     *          - $ids: list of entity IDs that require value generation
2105     *          - &$ttls: reference to the (entity ID => new TTL) map (alterable)
2106     *          - &$setOpts: reference to the new value set() options (alterable)
2107     *   - c) The return value is a map of (cache key => value) in the order of $keyedIds
2108     *   - d) The "lockTSE" and "busyValue" options are ignored
2109     *
2110     * @see WANObjectCache::getWithSetCallback()
2111     * @see WANObjectCache::getMultiWithSetCallback()
2112     *
2113     * Example usage:
2114     * @code
2115     *     $rows = $cache->getMultiWithUnionSetCallback(
2116     *         // Map of cache keys to entity IDs
2117     *         $cache->makeMultiKeys(
2118     *             $this->fileVersionIds(),
2119     *             function ( $id ) use ( $cache ) {
2120     *                 return $cache->makeKey( 'file-version', $id );
2121     *             }
2122     *         ),
2123     *         // Time-to-live (in seconds)
2124     *         $cache::TTL_DAY,
2125     *         // Function that derives the new key value
2126     *         function ( array $ids, array &$ttls, array &$setOpts ) {
2127     *             $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
2128     *             // Account for any snapshot/replica DB lag
2129     *             $setOpts += Database::getCacheSetOptions( $dbr );
2130     *
2131     *             // Load the rows for these files
2132     *             $rows = array_fill_keys( $ids, false );
2133     *             $queryInfo = File::getQueryInfo();
2134     *             $res = $dbr->select(
2135     *                 $queryInfo['tables'],
2136     *                 $queryInfo['fields'],
2137     *                 [ 'id' => $ids ],
2138     *                 __METHOD__,
2139     *                 [],
2140     *                 $queryInfo['joins']
2141     *             );
2142     *             foreach ( $res as $row ) {
2143     *                 $rows[$row->id] = $row;
2144     *                 $mtime = wfTimestamp( TS::UNIX, $row->timestamp );
2145     *                 $ttls[$row->id] = $this->adaptiveTTL( $mtime, $ttls[$row->id] );
2146     *             }
2147     *
2148     *             return $rows;
2149     *         },
2150     *         ]
2151     *     );
2152     *     $files = array_map( self::newFromRow( ... ), $rows );
2153     * @endcode
2154     *
2155     * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
2156     * @param int $ttl Seconds to live for key updates
2157     * @param callable $callback Callback that yields entity generation callbacks
2158     * @param array $opts Options map similar to that of getWithSetCallback()
2159     * @return mixed[] Map of (cache key => value) in the same order as $keyedIds
2160     * @since 1.30
2161     */
2162    final public function getMultiWithUnionSetCallback(
2163        ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
2164    ) {
2165        $span = $this->startOperationSpan( __FUNCTION__, '' );
2166        if ( $span->getContext()->isSampled() ) {
2167            $span->setAttributes( [
2168                'org.wikimedia.wancache.multi_count' => $keyedIds->count(),
2169                'org.wikimedia.wancache.ttl' => $ttl,
2170            ] );
2171        }
2172        $checkKeys = $opts['checkKeys'] ?? [];  // TODO: ???
2173        $minAsOf = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
2174
2175        // unset incompatible keys
2176        unset( $opts['lockTSE'] );
2177        unset( $opts['busyValue'] );
2178
2179        // Batch load required keys into the in-process warmup cache
2180        $keysByIdGet = $this->getNonProcessCachedMultiKeys( $keyedIds, $opts );
2181        $this->warmupCache = $this->fetchWrappedValuesForWarmupCache( $keysByIdGet, $checkKeys );
2182        $this->warmupKeyMisses = 0;
2183
2184        // IDs of entities known to be in need of generation
2185        $idsRegen = [];
2186
2187        // Find out which keys are missing/deleted/stale
2188        $now = $this->getCurrentTime();
2189        $resByKey = $this->fetchKeys( $keysByIdGet, $checkKeys, $now );
2190        foreach ( $keysByIdGet as $id => $key ) {
2191            $res = $resByKey[$key];
2192            if (
2193                $res[self::RES_VALUE] === false ||
2194                $res[self::RES_CUR_TTL] < 0 ||
2195                $res[self::RES_AS_OF] < $minAsOf
2196            ) {
2197                $idsRegen[] = $id;
2198            }
2199        }
2200
2201        // Run the callback to populate the generation value map for all required IDs
2202        $newSetOpts = [];
2203        $newTTLsById = array_fill_keys( $idsRegen, $ttl );
2204        $newValsById = $idsRegen ? $callback( $idsRegen, $newTTLsById, $newSetOpts ) : [];
2205
2206        $method = __METHOD__;
2207        // The required callback signature includes $id as the first argument for convenience
2208        // to distinguish different items. To reuse the code in getWithSetCallback(), wrap the
2209        // callback with a proxy callback that has the standard getWithSetCallback() signature.
2210        // This is defined only once per batch to avoid closure creation overhead.
2211        $proxyCb = function ( $oldValue, &$ttl, &$setOpts, $oldAsOf, $params )
2212            use ( $callback, $newValsById, $newTTLsById, $newSetOpts, $method )
2213        {
2214            $id = $params['id'];
2215
2216            if ( array_key_exists( $id, $newValsById ) ) {
2217                // Value was already regenerated as expected, so use the value in $newValsById
2218                $newValue = $newValsById[$id];
2219                $ttl = $newTTLsById[$id];
2220                $setOpts = $newSetOpts;
2221            } else {
2222                // Pre-emptive/popularity refresh and version mismatch cases are not detected
2223                // above and thus $newValsById has no entry. Run $callback on this single entity.
2224                $ttls = [ $id => $ttl ];
2225                $result = $callback( [ $id ], $ttls, $setOpts );
2226                if ( !isset( $result[$id] ) ) {
2227                    // T303092
2228                    $this->logger->warning(
2229                        $method . ' failed due to {id} not set in result {result}', [
2230                            'id' => $id,
2231                            'result' => json_encode( $result )
2232                        ] );
2233                }
2234                $newValue = $result[$id];
2235                $ttl = $ttls[$id];
2236            }
2237
2238            return $newValue;
2239        };
2240
2241        // Get the order-preserved result map using the warm-up cache
2242        $values = [];
2243        foreach ( $keyedIds as $key => $id ) {
2244            $values[$key] = $this->getWithSetCallback(
2245                $key,
2246                $ttl,
2247                $proxyCb,
2248                $opts,
2249                [ 'id' => $id ]
2250            );
2251        }
2252
2253        $this->warmupCache = [];
2254
2255        return $values;
2256    }
2257
2258    /**
2259     * @see BagOStuff::makeGlobalKey()
2260     * @since 1.27
2261     * @param string $keygroup Key group component, should be under 48 characters.
2262     * @param string|int ...$components Additional, ordered, key components for entity IDs
2263     * @return string Colon-separated, keyspace-prepended, ordered list of encoded components
2264     */
2265    public function makeGlobalKey( $keygroup, ...$components ) {
2266        return $this->cache->makeGlobalKey( $keygroup, ...$components );
2267    }
2268
2269    /**
2270     * @see BagOStuff::makeKey()
2271     * @since 1.27
2272     * @param string $keygroup Key group component, should be under 48 characters.
2273     * @param string|int ...$components Additional, ordered, key components for entity IDs
2274     * @return string Colon-separated, keyspace-prepended, ordered list of encoded components
2275     */
2276    public function makeKey( $keygroup, ...$components ) {
2277        return $this->cache->makeKey( $keygroup, ...$components );
2278    }
2279
2280    /**
2281     * Get an iterator of (cache key => entity ID) for a list of entity IDs
2282     *
2283     * The $callback argument expects a function that returns the key for an entity ID via
2284     * makeKey()/makeGlobalKey(). There should be no network nor filesystem I/O used in the
2285     * callback. The entity ID/key mapping must be 1:1 or an exception will be thrown.
2286     *
2287     * The callback takes the following arguments:
2288     *   - $id: An entity ID
2289     *   - $cache: This WANObjectCache instance
2290     *
2291     * Example usage for the default keyspace:
2292     * @code
2293     *     $keyedIds = $cache->makeMultiKeys(
2294     *         $urls,
2295     *         function ( $url, $cache ) {
2296     *             return $cache->makeKey( 'example-url', $url );
2297     *         }
2298     *     );
2299     * @endcode
2300     *
2301     * Example usage for mixed default and global keyspace:
2302     * @code
2303     *     $keyedIds = $cache->makeMultiKeys(
2304     *         $filters,
2305     *         function ( $filter, $cache ) {
2306     *             return self::isCentral( $filter )
2307     *                 ? $cache->makeGlobalKey( 'example-filter', $filter )
2308     *                 : $cache->makeKey( 'example-filter', $filter )
2309     *         }
2310     *     );
2311     * @endcode
2312     *
2313     * @see WANObjectCache::makeKey()
2314     * @see WANObjectCache::makeGlobalKey()
2315     *
2316     * @param string[]|int[] $ids List of entity IDs
2317     * @param callable $keyCallback Function returning makeKey()/makeGlobalKey() on the input ID
2318     * @return ArrayIterator Iterator of (cache key => ID); order of $ids is preserved
2319     * @since 1.28
2320     */
2321    final public function makeMultiKeys( array $ids, $keyCallback ) {
2322        $idByKey = [];
2323        foreach ( $ids as $id ) {
2324            $key = $keyCallback( $id, $this );
2325            // Edge case: ignore key collisions due to duplicate $ids like "42" and 42
2326            if ( !isset( $idByKey[$key] ) ) {
2327                $idByKey[$key] = $id;
2328            } elseif ( (string)$id !== (string)$idByKey[$key] ) {
2329                throw new UnexpectedValueException(
2330                    "Cache key collision; IDs ('$id','{$idByKey[$key]}') map to '$key'"
2331                );
2332            }
2333        }
2334
2335        return new ArrayIterator( $idByKey );
2336    }
2337
2338    /**
2339     * Get an (ID => value) map from (i) a non-unique list of entity IDs, and (ii) the list
2340     * of corresponding entity values by first appearance of each ID in the entity ID list
2341     *
2342     * For use with getMultiWithSetCallback() and getMultiWithUnionSetCallback().
2343     *
2344     * *Only* use this method if the entity ID/key mapping is trivially 1:1 without exception.
2345     * Key generation method must utilize the *full* entity ID in the key (not a hash of it).
2346     *
2347     * Example usage:
2348     * @code
2349     *     $poems = $cache->getMultiWithSetCallback(
2350     *         $cache->makeMultiKeys(
2351     *             $uuids,
2352     *             function ( $uuid ) use ( $cache ) {
2353     *                 return $cache->makeKey( 'poem', $uuid );
2354     *             }
2355     *         ),
2356     *         $cache::TTL_DAY,
2357     *         function ( $uuid ) use ( $url ) {
2358     *             return $this->http->run( [ 'method' => 'GET', 'url' => "$url/$uuid" ] );
2359     *         }
2360     *     );
2361     *     $poemsByUUID = $cache->multiRemap( $uuids, $poems );
2362     * @endcode
2363     *
2364     * @see WANObjectCache::makeMultiKeys()
2365     * @see WANObjectCache::getMultiWithSetCallback()
2366     * @see WANObjectCache::getMultiWithUnionSetCallback()
2367     *
2368     * @param string[]|int[] $ids Entity ID list makeMultiKeys()
2369     * @param mixed[] $res Result of getMultiWithSetCallback()/getMultiWithUnionSetCallback()
2370     * @return mixed[] Map of (ID => value); order of $ids is preserved
2371     * @since 1.34
2372     */
2373    final public function multiRemap( array $ids, array $res ) {
2374        if ( count( $ids ) !== count( $res ) ) {
2375            // If makeMultiKeys() is called on a list of non-unique IDs, then the resulting
2376            // ArrayIterator will have less entries due to "first appearance" de-duplication
2377            $ids = array_keys( array_fill_keys( $ids, true ) );
2378            if ( count( $ids ) !== count( $res ) ) {
2379                throw new UnexpectedValueException( "Multi-key result does not match ID list" );
2380            }
2381        }
2382
2383        return array_combine( $ids, $res );
2384    }
2385
2386    /**
2387     * Get a "watch point" token that can be used to get the "last error" to occur after now
2388     *
2389     * @return int A token that the current error event
2390     * @since 1.38
2391     */
2392    public function watchErrors() {
2393        return $this->cache->watchErrors();
2394    }
2395
2396    /**
2397     * Get the "last error" registry
2398     *
2399     * The method should be invoked by a caller as part of the following pattern:
2400     *   - The caller invokes watchErrors() to get a "since token"
2401     *   - The caller invokes a sequence of cache operation methods
2402     *   - The caller invokes getLastError() with the "since token"
2403     *
2404     * External callers can also invoke this method as part of the following pattern:
2405     *   - The caller invokes clearLastError()
2406     *   - The caller invokes a sequence of cache operation methods
2407     *   - The caller invokes getLastError()
2408     *
2409     * @param int $watchPoint Only consider errors from after this "watch point" [optional]
2410     * @return int BagOStuff:ERR_* constant for the "last error" registry
2411     * @note Parameters added in 1.38: $watchPoint
2412     */
2413    final public function getLastError( $watchPoint = 0 ) {
2414        $code = $this->cache->getLastError( $watchPoint );
2415        switch ( $code ) {
2416            case BagOStuff::ERR_NONE:
2417                return BagOStuff::ERR_NONE;
2418            case BagOStuff::ERR_NO_RESPONSE:
2419                return BagOStuff::ERR_NO_RESPONSE;
2420            case BagOStuff::ERR_UNREACHABLE:
2421                return BagOStuff::ERR_UNREACHABLE;
2422            default:
2423                return BagOStuff::ERR_UNEXPECTED;
2424        }
2425    }
2426
2427    /**
2428     * Clear the in-process caches; useful for testing
2429     *
2430     * @since 1.27
2431     */
2432    public function clearProcessCache() {
2433        $this->processCaches = [];
2434    }
2435
2436    /**
2437     * Enable or disable the use of brief caching for tombstoned keys
2438     *
2439     * When a key is purged via delete(), there normally is a period where caching
2440     * is hold-off limited to an extremely short time. This method will disable that
2441     * caching, forcing the callback to run for any of:
2442     *   - WANObjectCache::getWithSetCallback()
2443     *   - WANObjectCache::getMultiWithSetCallback()
2444     *   - WANObjectCache::getMultiWithUnionSetCallback()
2445     *
2446     * This is useful when both:
2447     *   - a) the database used by the callback is known to be up-to-date enough
2448     *        for some particular purpose (e.g. replica DB has applied transaction X)
2449     *   - b) the caller needs to exploit that fact, and therefore needs to avoid the
2450     *        use of inherently volatile and possibly stale interim keys
2451     *
2452     * @see WANObjectCache::delete()
2453     * @param bool $enabled Whether to enable interim caching
2454     * @since 1.31
2455     */
2456    final public function useInterimHoldOffCaching( $enabled ) {
2457        $this->useInterimHoldOffCaching = $enabled;
2458    }
2459
2460    /**
2461     * @param int $flag BagOStuff::ATTR_* class constant
2462     * @return int BagOStuff::QOS_* class constant
2463     * @since 1.28
2464     */
2465    public function getQoS( $flag ) {
2466        return $this->cache->getQoS( $flag );
2467    }
2468
2469    /**
2470     * Get a TTL that is higher for objects that have not changed recently
2471     *
2472     * This is useful for keys that get explicit purges and DB or purge relay
2473     * lag is a potential concern (especially how it interacts with CDN cache)
2474     *
2475     * Example usage:
2476     * @code
2477     *     // Last-modified time of page
2478     *     $mtime = wfTimestamp( TS::UNIX, $page->getTimestamp() );
2479     *     // Get adjusted TTL. If $mtime is 3600 seconds ago and $minTTL/$factor left at
2480     *     // defaults, then $ttl is 3600 * .2 = 720. If $minTTL was greater than 720, then
2481     *     // $ttl would be $minTTL. If $maxTTL was smaller than 720, $ttl would be $maxTTL.
2482     *     $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY );
2483     * @endcode
2484     *
2485     * Another use case is when there are no applicable "last modified" fields in the DB,
2486     * and there are too many dependencies for explicit purges to be viable, and the rate of
2487     * change to relevant content is unstable, and it is highly valued to have the cached value
2488     * be as up-to-date as possible.
2489     *
2490     * Example usage:
2491     * @code
2492     *     $query = "<some complex query>";
2493     *     $idListFromComplexQuery = $cache->getWithSetCallback(
2494     *         $cache->makeKey( 'complex-graph-query', $hashOfQuery ),
2495     *         GraphQueryClass::STARTING_TTL,
2496     *         function ( $oldValue, &$ttl, array &$setOpts, $oldAsOf ) use ( $query, $cache ) {
2497     *             $gdb = $this->getReplicaGraphDbConnection();
2498     *             // Account for any snapshot/replica DB lag
2499     *             $setOpts += GraphDatabase::getCacheSetOptions( $gdb );
2500     *
2501     *             $newList = iterator_to_array( $gdb->query( $query ) );
2502     *             sort( $newList, SORT_NUMERIC ); // normalize
2503     *
2504     *             $minTTL = GraphQueryClass::MIN_TTL;
2505     *             $maxTTL = GraphQueryClass::MAX_TTL;
2506     *             if ( $oldValue !== false ) {
2507     *                 // Note that $oldAsOf is the last time this callback ran
2508     *                 $ttl = ( $newList === $oldValue )
2509     *                     // No change: cache for 150% of the age of $oldValue
2510     *                     ? $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, 1.5 )
2511     *                     // Changed: cache for 50% of the age of $oldValue
2512     *                     : $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, .5 );
2513     *             }
2514     *
2515     *             return $newList;
2516     *        },
2517     *        [
2518     *             // Keep stale values around for doing comparisons for TTL calculations.
2519     *             // High values improve long-tail keys hit-rates, though might waste space.
2520     *             'staleTTL' => GraphQueryClass::GRACE_TTL
2521     *        ]
2522     *     );
2523     * @endcode
2524     *
2525     * @param int|float|string|null $mtime UNIX timestamp; null if none
2526     * @param int $maxTTL Maximum TTL (seconds)
2527     * @param int $minTTL Minimum TTL (seconds); Default: 30
2528     * @param float $factor Value in the range (0,1); Default: .2
2529     * @return int Adaptive TTL
2530     * @since 1.28
2531     */
2532    public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = 0.2 ) {
2533        // handle fractional seconds and string integers
2534        $mtime = (int)$mtime;
2535        if ( $mtime <= 0 ) {
2536            // no last-modified time provided
2537            return $minTTL;
2538        }
2539
2540        $age = (int)$this->getCurrentTime() - $mtime;
2541
2542        return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
2543    }
2544
2545    /**
2546     * @internal For use by unit tests only
2547     * @return int
2548     * @since 1.30
2549     */
2550    final public function getWarmupKeyMisses() {
2551        // Number of misses in $this->warmupCache during the last call to certain methods
2552        return $this->warmupKeyMisses;
2553    }
2554
2555    /**
2556     * @param string $sisterKey
2557     * @return string
2558     */
2559    protected function getRouteKey( string $sisterKey ) {
2560        if ( $this->broadcastRoute !== null ) {
2561            if ( $sisterKey[0] === '/' ) {
2562                throw new RuntimeException( "Sister key '$sisterKey' already contains a route." );
2563            }
2564            return $this->broadcastRoute . $sisterKey;
2565        }
2566        return $sisterKey;
2567    }
2568
2569    /**
2570     * Schedule a deferred cache regeneration if possible
2571     *
2572     * @param string $key Cache key made with makeKey()/makeGlobalKey()
2573     * @param int $ttl Seconds to live
2574     * @param callable $callback
2575     * @param array $opts
2576     * @param array $cbParams
2577     * @return bool Success
2578     * @note Callable type hints are not used to avoid class-autoloading
2579     */
2580    private function scheduleAsyncRefresh( $key, $ttl, $callback, array $opts, array $cbParams ) {
2581        if ( !$this->asyncHandler ) {
2582            return false;
2583        }
2584        // Update the cache value later, such during post-send of an HTTP request. This forces
2585        // cache regeneration by setting "minAsOf" to infinity, meaning that no existing value
2586        // is considered valid. Furthermore, note that preemptive regeneration is not applicable
2587        // to invalid values, so there is no risk of infinite preemptive regeneration loops.
2588        $func = $this->asyncHandler;
2589        $func( function () use ( $key, $ttl, $callback, $opts, $cbParams ) {
2590            $opts['minAsOf'] = INF;
2591            try {
2592                $this->fetchOrRegenerate( $key, $ttl, $callback, $opts, $cbParams );
2593            } catch ( Exception $e ) {
2594                // Log some context for easier debugging
2595                $this->logger->error( 'Async refresh failed for {key}', [
2596                    'key' => $key,
2597                    'ttl' => $ttl,
2598                    'exception' => $e
2599                ] );
2600                throw $e;
2601            }
2602        } );
2603
2604        return true;
2605    }
2606
2607    /**
2608     * Check if a key value is non-false, new enough, and either fresh or "gracefully" stale
2609     *
2610     * @param array $res Current value WANObjectCache::RES_* data map
2611     * @param int $graceTTL Consider using stale values if $curTTL is greater than this
2612     * @param float $minAsOf Minimum acceptable value "as of" UNIX timestamp
2613     * @return bool
2614     */
2615    private function isAcceptablyFreshValue( $res, $graceTTL, $minAsOf ) {
2616        if ( !$this->isValid( $res[self::RES_VALUE], $res[self::RES_AS_OF], $minAsOf ) ) {
2617            // Value does not exists or is too old
2618            return false;
2619        }
2620
2621        $curTTL = $res[self::RES_CUR_TTL];
2622        if ( $curTTL > 0 ) {
2623            // Value is definitely still fresh
2624            return true;
2625        }
2626
2627        // Remaining seconds during which this stale value can be used
2628        $curGraceTTL = $graceTTL + $curTTL;
2629
2630        return ( $curGraceTTL > 0 )
2631            // Chance of using the value decreases as $curTTL goes from 0 to -$graceTTL
2632            ? !$this->worthRefreshExpiring( $curGraceTTL, $graceTTL, $graceTTL )
2633            // Value is too stale to fall in the grace period
2634            : false;
2635    }
2636
2637    /**
2638     * Check if a key is due for randomized regeneration due to near-expiration/popularity
2639     *
2640     * @param array $res Current value WANObjectCache::RES_* data map
2641     * @param float $lowTTL Consider a refresh when $curTTL is less than this; the "low" threshold
2642     * @param int $ageNew Age of key when this might recommend refreshing (seconds)
2643     * @param int $hotTTR Age of key when it should be refreshed if popular (seconds)
2644     * @param float $now The current UNIX timestamp
2645     * @return bool
2646     */
2647    protected function isLotteryRefreshDue( $res, $lowTTL, $ageNew, $hotTTR, $now ) {
2648        $curTTL = $res[self::RES_CUR_TTL];
2649        $logicalTTL = $res[self::RES_TTL];
2650        $asOf = $res[self::RES_AS_OF];
2651
2652        return (
2653            $this->worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) ||
2654            $this->worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now )
2655        );
2656    }
2657
2658    /**
2659     * Check if a key is due for randomized regeneration due to its popularity
2660     *
2661     * Suppose we cache a value with a TTL of 50min (3000 seconds) and use the default
2662     * ageNew=60s and hotTTR=900s options. During the first 60s your key is safe from refreshes.
2663     * Then we ramp up the chance for the 30s from age 60s to 90s. After that, we apply a constant
2664     * chance until the key expires. The chance is calculated such that if your key is very hot
2665     * (>1 req/s), we automatically refresh it roughly once every 900s.
2666     *
2667     * See also WANObjectCache::worthRefreshExpiring which plays a role closer to the expiry.
2668     *
2669     * The chance is calculated as follows: At 1 req/s, a 1:900 chance of refresh should trigger
2670     * every 900s. We adjust for 60s being safe (ageNew) and 30s lower chance (RAMPUP_TTL), which
2671     * works out to 1:825 requests, or a 0.1212% chance to truly trigger every 900s.
2672     *
2673     * | Age  | Description
2674     * | ---- | -----------
2675     * | 0s   | miss, regen, set
2676     * | 1s   | get, hit (0% chance of async regen)
2677     * | 60s  | get, hit (0% chance of async regen)
2678     * | 63s  | get, hit (0.01212% chance of async regen, or 1:8250 requests)
2679     * | 70s  | get, hit (0.04040% chance of async regen, or 1:2475 requests)
2680     * | 90s  | get, hit (0.12% chance of async regen, or 1:825 requests)
2681     * | >90s | get, hit (0.12% chance of async regen, or 1:825 requests)
2682     *
2683     * This feature exists to reduce negative impact of lost or delayed cache purges. The impact
2684     * of a heavily referenced key being stale is worse than that of a rarely referenced key.
2685     * Unlike simply lowering $ttl, rare keys are largely unaffected to allow for a high cache-hit
2686     * ratio for the "long-tail" of less-used keys. Similar to worthRefreshExpiring(), this uses
2687     * randomization to avoid causing cache stampedes.
2688     *
2689     * @param float $asOf UNIX timestamp of the value
2690     * @param int $ageNew Age of key when this may recommend a refresh (seconds)
2691     * @param int $hotTTR Age of key by which this should recommend a refresh if popular (seconds)
2692     * @param float $now The current UNIX timestamp
2693     * @return bool
2694     */
2695    protected function worthRefreshPopular( $asOf, $ageNew, $hotTTR, $now ) {
2696        if ( $ageNew < 0 || $hotTTR <= 0 ) {
2697            return false;
2698        }
2699
2700        $age = $now - $asOf;
2701        $timeOld = $age - $ageNew;
2702        if ( $timeOld <= 0 ) {
2703            return false;
2704        }
2705
2706        $popularHitsPerSec = 1;
2707        // Lifecycle is: new, ramp-up refresh chance, full refresh chance.
2708        // Note that the "expected # of refreshes" for the ramp-up time range is half
2709        // of what it would be if P(refresh) was at its full value during that time range.
2710        $refreshWindowSec = max( $hotTTR - $ageNew - self::RAMPUP_TTL / 2, 1 );
2711        // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
2712        // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1 (by definition)
2713        // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
2714        $chance = 1 / ( $popularHitsPerSec * $refreshWindowSec );
2715        // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
2716        $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
2717
2718        return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2719    }
2720
2721    /**
2722     * Check if a key is nearing expiration and thus due for randomized regeneration.
2723     *
2724     * Suppose we cache a value with a TTL of 50min (3000 seconds), and a lowTTL of 100s.
2725     *
2726     * | Time passed | Remaining TTL | Description
2727     * | ----------- | ------------- | -----------
2728     * | t=0         | 3000s         | miss, regen, set
2729     * | t=100       | 2900s         | get, hit
2730     * | t=2899      | 101s          | get, hit
2731     * | t=2900      | 100s          | get, hit (0% chance of async regen)
2732     * | t=2950      | 50s           | get, hit (6% chance of async regen)
2733     * | t=2975      | 25s           | get, hit (31% chance of async regen)
2734     * | t=2995      | 5s            | get, hit (81% chance of async regen)
2735     * | t=2999      | 1s            | get, hit (96% chance of async regen)
2736     * | t=3000      | 0s            | miss, regen, set
2737     *
2738     * We only consider randomized regeneration once $curTTL is less than $lowTTL (i.e. near expiry).
2739     * We do not consider it once $curTTL is <= 0 (i.e. value expired).
2740     *
2741     * The chance of returning true increases steadily from 0% to 100% as
2742     * $curTTL counts down from the "low" threshold to 0 seconds.
2743     *
2744     * If the original TTL passed to WANObjectCache::getWithSetCallback is below $lowTTL (the author
2745     * choose a TTL under 60s, and did not set a custom 'lowTTL' option), then we use the original
2746     * TTL as the low threshold. This ensures the ramp up always starts slowly at 0%,
2747     * instead of breaking caching for intentionally short TTLs (T264787).
2748     *
2749     * This method uses deadline-aware randomization in order to handle wide ranges
2750     * of request rates without the need for complex options or state keeping.
2751     *
2752     * @param float $curTTL Approximate TTL left on the key
2753     * @param float $logicalTTL Full logical TTL assigned to the key; 0 for "infinite"
2754     * @param float $lowTTL Consider a refresh once $curTTL is less than this; the "low" threshold
2755     * @return bool
2756     */
2757    protected function worthRefreshExpiring( $curTTL, $logicalTTL, $lowTTL ) {
2758        if ( $lowTTL <= 0 ) {
2759            return false;
2760        }
2761        // T264787: avoid having keys start off with a high chance of being refreshed;
2762        // the point where refreshing becomes possible cannot precede the key lifetime.
2763        $effectiveLowTTL = min( $lowTTL, $logicalTTL ?: INF );
2764
2765        // How long the value was in the "low TTL" phase
2766        $timeOld = $effectiveLowTTL - $curTTL;
2767        if ( $timeOld <= 0 || $timeOld >= $effectiveLowTTL ) {
2768            return false;
2769        }
2770
2771        // Ratio of the low TTL phase that has elapsed (r)
2772        $ttrRatio = $timeOld / $effectiveLowTTL;
2773        // Use p(r) as the monotonically increasing "chance of refresh" function,
2774        // having p(0)=0 and p(1)=1. The value expires at the nominal expiry.
2775        $chance = $ttrRatio ** 4;
2776
2777        return ( mt_rand( 1, 1_000_000_000 ) <= 1_000_000_000 * $chance );
2778    }
2779
2780    /**
2781     * Check that a wrapper value exists and has an acceptable age
2782     *
2783     * @param array|false $value Value wrapper or false
2784     * @param float $asOf Value generation "as of" timestamp
2785     * @param float $minAsOf Minimum acceptable value "as of" UNIX timestamp
2786     * @return bool
2787     */
2788    protected function isValid( $value, $asOf, $minAsOf ) {
2789        return ( $value !== false && $asOf >= $minAsOf );
2790    }
2791
2792    /**
2793     * @param mixed $value
2794     * @param int $ttl Seconds to live or zero for "indefinite"
2795     * @param int|null $version Value version number or null if not versioned
2796     * @param float $now Unix Current timestamp just before calling set()
2797     * @return array
2798     */
2799    private function wrap( $value, $ttl, $version, $now ) {
2800        // Returns keys in ascending integer order for PHP7 array packing:
2801        // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2802        $wrapped = [
2803            self::FLD_FORMAT_VERSION => self::VERSION,
2804            self::FLD_VALUE => $value,
2805            self::FLD_TTL => $ttl,
2806            self::FLD_TIME => $now
2807        ];
2808        if ( $version !== null ) {
2809            $wrapped[self::FLD_VALUE_VERSION] = $version;
2810        }
2811
2812        return $wrapped;
2813    }
2814
2815    /**
2816     * @param array|string|false $wrapped The entry at a cache key (false if key is nonexistant)
2817     * @param float $now Unix Current timestamp (preferably pre-query)
2818     * @return array<int,mixed> Result map/n-tuple that includes the following:
2819     *   - WANObjectCache::RES_VALUE: value or false if absent/tombstoned/malformed
2820     *   - WANObjectCache::KEY_VERSION: value version number; null if there is no value
2821     *   - WANObjectCache::KEY_AS_OF: value generation timestamp (UNIX); null if there is no value
2822     *   - WANObjectCache::KEY_TTL: assigned logical TTL (seconds); null if there is no value
2823     *   - WANObjectCache::KEY_TOMB_AS_OF: tombstone timestamp (UNIX); null if not tombstoned
2824     *   - WANObjectCache::RES_CHECK_AS_OF: null placeholder for highest "check" key timestamp
2825     *   - WANObjectCache::RES_TOUCH_AS_OF: null placeholder for highest "touched" timestamp
2826     *   - WANObjectCache::KEY_CUR_TTL: remaining logical TTL (seconds) (negative if tombstoned)
2827     * @phan-return array{0:mixed,1:mixed,2:?float,3:?int,4:?float,5:?float,6:?float,7:?float}
2828     */
2829    private function unwrap( $wrapped, $now ) {
2830        // https://nikic.github.io/2014/12/22/PHPs-new-hashtable-implementation.html
2831        $res = [
2832            // Attributes that only depend on the fetched key value
2833            self::RES_VALUE => false,
2834            self::RES_VERSION => null,
2835            self::RES_AS_OF => null,
2836            self::RES_TTL => null,
2837            self::RES_TOMB_AS_OF => null,
2838            // Attributes that depend on caller-specific "check" keys or "touched callbacks"
2839            self::RES_CHECK_AS_OF => null,
2840            self::RES_TOUCH_AS_OF => null,
2841            self::RES_CUR_TTL => null
2842        ];
2843
2844        if ( is_array( $wrapped ) ) {
2845            // Entry expected to be a cached value; validate it
2846            if (
2847                ( $wrapped[self::FLD_FORMAT_VERSION] ?? null ) === self::VERSION &&
2848                $wrapped[self::FLD_TIME] >= $this->epoch
2849            ) {
2850                if ( $wrapped[self::FLD_TTL] > 0 ) {
2851                    // Get the approximate time left on the key
2852                    $age = $now - $wrapped[self::FLD_TIME];
2853                    $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );
2854                } else {
2855                    // Key had no TTL, so the time left is unbounded
2856                    $curTTL = INF;
2857                }
2858                $res[self::RES_VALUE] = $wrapped[self::FLD_VALUE];
2859                $res[self::RES_VERSION] = $wrapped[self::FLD_VALUE_VERSION] ?? null;
2860                $res[self::RES_AS_OF] = $wrapped[self::FLD_TIME];
2861                $res[self::RES_CUR_TTL] = $curTTL;
2862                $res[self::RES_TTL] = $wrapped[self::FLD_TTL];
2863            }
2864        } else {
2865            // Entry expected to be a tombstone; parse it
2866            $purge = $this->parsePurgeValue( $wrapped );
2867            if ( $purge !== null ) {
2868                // Tombstoned keys should always have a negative "current TTL"
2869                $curTTL = min( $purge[self::PURGE_TIME] - $now, self::TINY_NEGATIVE );
2870                $res[self::RES_CUR_TTL] = $curTTL;
2871                $res[self::RES_TOMB_AS_OF] = $purge[self::PURGE_TIME];
2872            }
2873        }
2874
2875        return $res;
2876    }
2877
2878    /**
2879     * @param string $key Cache key in the format `<keyspace>:<keygroup>[:<other components>]...`
2880     *  as formatted by WANObjectCache::makeKey() or ::makeKeyGlobal.
2881     * @return string The key group of this cache key
2882     */
2883    private function determineKeyGroupForStats( $key ) {
2884        $parts = explode( ':', $key, 3 );
2885        // Fallback in case the key was not made by makeKey.
2886        // Replace dots because they are special in StatsD (T232907)
2887        return strtr( $parts[1] ?? $parts[0], '.', '_' );
2888    }
2889
2890    /**
2891     * Extract purge metadata from cached value if it is a valid purge value
2892     *
2893     * Valid purge values come from makeTombstonePurgeValue()/makeCheckKeyPurgeValue()
2894     *
2895     * @param mixed $value Cached value
2896     * @return array|null Tuple of (UNIX timestamp, hold-off seconds); null if value is invalid
2897     */
2898    private function parsePurgeValue( $value ) {
2899        if ( !is_string( $value ) ) {
2900            return null;
2901        }
2902
2903        $segments = explode( ':', $value, 3 );
2904        $prefix = $segments[0];
2905        if ( $prefix !== self::PURGE_VAL_PREFIX ) {
2906            // Not a purge value
2907            return null;
2908        }
2909
2910        $timestamp = (float)$segments[1];
2911        // makeTombstonePurgeValue() doesn't store hold-off TTLs
2912        $holdoff = isset( $segments[2] ) ? (int)$segments[2] : self::HOLDOFF_TTL;
2913
2914        if ( $timestamp < $this->epoch ) {
2915            // Purge value is too old
2916            return null;
2917        }
2918
2919        return [ self::PURGE_TIME => $timestamp, self::PURGE_HOLDOFF => $holdoff ];
2920    }
2921
2922    /**
2923     * @param float $timestamp UNIX timestamp
2924     * @param int $holdoff In seconds
2925     * @param array|null &$purge Unwrapped purge value array [returned]
2926     * @return string Wrapped purge value; format is "PURGED:<timestamp>:<holdoff>"
2927     */
2928    private function makeCheckPurgeValue( float $timestamp, int $holdoff, ?array &$purge = null ) {
2929        $normalizedTime = (int)$timestamp;
2930        // Purge array that matches what parsePurgeValue() would have returned
2931        $purge = [ self::PURGE_TIME => (float)$normalizedTime, self::PURGE_HOLDOFF => $holdoff ];
2932
2933        return self::PURGE_VAL_PREFIX . ":$normalizedTime:$holdoff";
2934    }
2935
2936    /**
2937     * @param string $group
2938     * @return MapCacheLRU
2939     */
2940    private function getProcessCache( $group ) {
2941        if ( !isset( $this->processCaches[$group] ) ) {
2942            [ , $size ] = explode( ':', $group );
2943            $this->processCaches[$group] = new MapCacheLRU( (int)$size );
2944            if ( $this->wallClockOverride !== null ) {
2945                $this->processCaches[$group]->setMockTime( $this->wallClockOverride );
2946            }
2947        }
2948
2949        return $this->processCaches[$group];
2950    }
2951
2952    /**
2953     * @param ArrayIterator $keys
2954     * @param array $opts
2955     * @return string[] Map of (ID => cache key)
2956     */
2957    private function getNonProcessCachedMultiKeys( ArrayIterator $keys, array $opts ) {
2958        $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
2959
2960        $keysMissing = [];
2961        if ( $pcTTL > 0 && $this->callbackDepth == 0 ) {
2962            $pCache = $this->getProcessCache( $opts['pcGroup'] ?? self::PC_PRIMARY );
2963            foreach ( $keys as $key => $id ) {
2964                if ( !$pCache->has( $key, $pcTTL ) ) {
2965                    $keysMissing[$id] = $key;
2966                }
2967            }
2968        }
2969
2970        return $keysMissing;
2971    }
2972
2973    /**
2974     * @param string[] $keys Cache keys made with makeKey()/makeGlobalKey()
2975     * @param string[]|string[][] $checkKeys Map of (integer or cache key => "check" key(s));
2976     *  "check" keys must also be made with makeKey()/makeGlobalKey()
2977     * @return array<string,mixed> Map of (sister key => value, or, false if not found)
2978     */
2979    private function fetchWrappedValuesForWarmupCache( array $keys, array $checkKeys ) {
2980        if ( !$keys ) {
2981            return [];
2982        }
2983
2984        // Get all the value keys to fetch...
2985        $sisterKeys = [];
2986        foreach ( $keys as $baseKey ) {
2987            $sisterKeys[] = $this->makeSisterKey( $baseKey, self::TYPE_VALUE );
2988        }
2989        // Get all the "check" keys to fetch...
2990        foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) {
2991            // Note: avoid array_merge() inside loop in case there are many keys
2992            if ( is_int( $i ) ) {
2993                // Single "check" key that applies to all value keys
2994                $sisterKeys[] = $this->makeSisterKey( $checkKeyOrKeyGroup, self::TYPE_TIMESTAMP );
2995            } else {
2996                // List of "check" keys that apply to a specific value key
2997                foreach ( (array)$checkKeyOrKeyGroup as $checkKey ) {
2998                    $sisterKeys[] = $this->makeSisterKey( $checkKey, self::TYPE_TIMESTAMP );
2999                }
3000            }
3001        }
3002
3003        $wrappedBySisterKey = $this->cache->getMulti( $sisterKeys );
3004        $wrappedBySisterKey += array_fill_keys( $sisterKeys, false );
3005
3006        return $wrappedBySisterKey;
3007    }
3008
3009    /**
3010     * @param string $key Cache key made with makeKey()/makeGlobalKey()
3011     * @param float $now Current UNIX timestamp
3012     * @return float|null Seconds since the last logged get() miss for this key, or, null
3013     */
3014    private function timeSinceLoggedMiss( $key, $now ) {
3015        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found
3016        for ( end( $this->missLog ); $miss = current( $this->missLog ); prev( $this->missLog ) ) {
3017            if ( $miss[0] === $key ) {
3018                return ( $now - $miss[1] );
3019            }
3020        }
3021
3022        return null;
3023    }
3024
3025    /**
3026     * @return float UNIX timestamp
3027     * @codeCoverageIgnore
3028     */
3029    protected function getCurrentTime() {
3030        return $this->wallClockOverride ?: microtime( true );
3031    }
3032
3033    /**
3034     * @param float|null &$time Mock UNIX timestamp for testing
3035     * @codeCoverageIgnore
3036     */
3037    public function setMockTime( &$time ) {
3038        $this->wallClockOverride =& $time;
3039        $this->cache->setMockTime( $time );
3040        foreach ( $this->processCaches as $pCache ) {
3041            $pCache->setMockTime( $time );
3042        }
3043    }
3044
3045    /**
3046     * Convenience function to start an OpenTelemetry span for the given operation.
3047     * The span is deactivated and ended once the returned object goes out of scope,
3048     * but to be sure of timings, callers should call finish() on the returned object.
3049     *
3050     * @param string $opName Name of the operation to instrument (e.g. GET)
3051     * @param string|string[] $keys Cache keys related to the operation
3052     * @param string[]|string[][] $checkKeys 1/2D array of check keys related to the operation
3053     *
3054     * @return SpanInterface
3055     */
3056    private function startOperationSpan( $opName, $keys, $checkKeys = [] ) {
3057        $span = $this->tracer->createSpan( "WANObjectCache::$opName" )
3058            ->setSpanKind( SpanInterface::SPAN_KIND_CLIENT )
3059            ->start();
3060
3061        if ( !$span->getContext()->isSampled() ) {
3062            return $span;
3063        }
3064
3065        $keys = is_array( $keys ) ? implode( ' ', $keys ) : $keys;
3066
3067        if ( count( $checkKeys ) > 0 ) {
3068            $checkKeys = array_map(
3069                static fn ( $checkKeyOrKeyGroup ) =>
3070                    is_array( $checkKeyOrKeyGroup )
3071                        ? implode( ' ', $checkKeyOrKeyGroup )
3072                        : $checkKeyOrKeyGroup,
3073                $checkKeys );
3074
3075            $checkKeys = implode( ' ', $checkKeys );
3076            $span->setAttributes( [ 'org.wikimedia.wancache.check_keys' => $checkKeys ] );
3077        }
3078
3079        $span->setAttributes( [ 'org.wikimedia.wancache.keys' => $keys ] );
3080
3081        $span->activate();
3082        return $span;
3083    }
3084}
3085
3086/** @deprecated class alias since 1.43 */
3087class_alias( WANObjectCache::class, 'WANObjectCache' );