Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
16.87% |
14 / 83 |
|
18.18% |
4 / 22 |
CRAP | |
0.00% |
0 / 1 |
BagOStuff | |
16.87% |
14 / 83 |
|
18.18% |
4 / 22 |
1428.45 | |
0.00% |
0 / 1 |
__construct | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWithSetCallback | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
get | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
set | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
delete | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
add | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
merge | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
changeTTL | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
lock | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
unlock | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getScopedLock | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
deleteObjectsExpiringBefore | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
deleteMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
changeTTLMulti | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
incrWithInit | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
watchErrors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
clearLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setLastError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeGlobalKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isKeyGlobal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQoS | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSegmentationSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSegmentedValueMaxSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fieldHasFlags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mergeFlagMaps | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
setNewPreparedValues | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
makeKeyInternal | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
requireConvertGenericKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
convertGenericKey | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
proxyCall | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
getCurrentTime | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
setMockTime | n/a |
0 / 0 |
n/a |
0 / 0 |
1 |
1 | <?php |
2 | /** |
3 | * Copyright © 2003-2004 Brooke Vibber <bvibber@wikimedia.org> |
4 | * https://www.mediawiki.org/ |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @ingroup Cache |
23 | */ |
24 | |
25 | /** |
26 | * @defgroup Cache Cache |
27 | */ |
28 | |
29 | use Psr\Log\LoggerAwareInterface; |
30 | use Psr\Log\LoggerInterface; |
31 | use Psr\Log\NullLogger; |
32 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
33 | use Wikimedia\LightweightObjectStore\StorageAwareness; |
34 | use Wikimedia\ScopedCallback; |
35 | use Wikimedia\Stats\StatsFactory; |
36 | |
37 | /** |
38 | * Class representing a cache/ephemeral data store |
39 | * |
40 | * This interface is intended to be more or less compatible with the PHP memcached client. |
41 | * |
42 | * Class instances should be created with an intended access scope for the dataset, such as: |
43 | * - a) A single PHP thread on a server (e.g. stored in a PHP variable) |
44 | * - b) A single application server (e.g. stored in APC or sqlite) |
45 | * - c) All application servers in datacenter (e.g. stored in memcached or mysql) |
46 | * - d) All application servers in all datacenters (e.g. stored via mcrouter or dynomite) |
47 | * |
48 | * Callers should use the proper factory methods that yield BagOStuff instances. Site admins |
49 | * should make sure that the configuration for those factory methods match their access scope. |
50 | * BagOStuff subclasses have widely varying levels of support replication features within and |
51 | * among datacenters. |
52 | * |
53 | * Subclasses should override the default "segmentationSize" field with an appropriate value. |
54 | * The value should not be larger than what the backing store (by default) supports. It also |
55 | * should be roughly informed by common performance bottlenecks (e.g. values over a certain size |
56 | * having poor scalability). The same goes for the "segmentedValueMaxSize" member, which limits |
57 | * the maximum size and chunk count (indirectly) of values. |
58 | * |
59 | * A few notes about data consistency for BagOStuff instances: |
60 | * - Read operation methods, e.g. get(), should be synchronous in the local datacenter. |
61 | * When used with READ_LATEST, such operations should reflect any prior writes originating |
62 | * from the local datacenter (e.g. by avoiding replica DBs or invoking quorom reads). |
63 | * - Write operation methods, e.g. set(), should be synchronous in the local datacenter, with |
64 | * asynchronous cross-datacenter replication. This replication can be either "best effort" |
65 | * or eventually consistent. If the write succeeded, then any subsequent `get()` operations with |
66 | * READ_LATEST, regardless of datacenter, should reflect the changes. |
67 | * - Locking operation methods, e.g. lock(), unlock(), and getScopedLock(), should only apply |
68 | * to the local datacenter. |
69 | * - Any set of single-key write operation method calls originating from a single datacenter |
70 | * should observe "best effort" linearizability. |
71 | * In this context, "best effort" means that consistency holds as long as connectivity is |
72 | * strong, network latency is low, and there are no relevant storage server failures. |
73 | * Per https://en.wikipedia.org/wiki/PACELC_theorem, the store should act as a PA/EL |
74 | * distributed system for these operations. |
75 | * |
76 | * @stable to extend |
77 | * @newable |
78 | * @ingroup Cache |
79 | */ |
80 | abstract class BagOStuff implements |
81 | ExpirationAwareness, |
82 | StorageAwareness, |
83 | IStoreKeyEncoder, |
84 | LoggerAwareInterface |
85 | { |
86 | /** @var StatsFactory */ |
87 | protected $stats; |
88 | /** @var LoggerInterface */ |
89 | protected $logger; |
90 | /** @var callable|null */ |
91 | protected $asyncHandler; |
92 | /** @var int[] Map of (BagOStuff:ATTR_* constant => BagOStuff:QOS_* constant) */ |
93 | protected $attrMap = []; |
94 | |
95 | /** @var string Default keyspace; used by makeKey() */ |
96 | protected $keyspace; |
97 | |
98 | /** @var int BagOStuff:ERR_* constant of the last error that occurred */ |
99 | protected $lastError = self::ERR_NONE; |
100 | /** @var int Error event sequence number of the last error that occurred */ |
101 | protected $lastErrorId = 0; |
102 | |
103 | /** @var int Next sequence number to use for watch/error events */ |
104 | protected static $nextErrorMonitorId = 1; |
105 | |
106 | /** @var float|null */ |
107 | private $wallClockOverride; |
108 | |
109 | /** Bitfield constants for get()/getMulti(); these are only advisory */ |
110 | /** If supported, avoid reading stale data due to replication */ |
111 | public const READ_LATEST = 1; |
112 | /** Promise that the caller handles detection of staleness */ |
113 | public const READ_VERIFIED = 2; |
114 | |
115 | /** Bitfield constants for set()/merge(); these are only advisory */ |
116 | /** Only change state of the in-memory cache */ |
117 | public const WRITE_CACHE_ONLY = 8; |
118 | /** Allow partitioning of the value if it is a large string */ |
119 | public const WRITE_ALLOW_SEGMENTS = 16; |
120 | /** Delete all the segments if the value is partitioned */ |
121 | public const WRITE_PRUNE_SEGMENTS = 32; |
122 | /** |
123 | * If supported, do not block on write operation completion; instead, treat writes as |
124 | * succesful based on whether they could be buffered. When using this flag with methods |
125 | * that yield item values, the boolean "true" will be used as a placeholder. The next |
126 | * blocking operation (e.g. typical read) will trigger a flush of the operation buffer. |
127 | */ |
128 | public const WRITE_BACKGROUND = 64; |
129 | |
130 | /** Abort after the first merge conflict */ |
131 | public const MAX_CONFLICTS_ONE = 1; |
132 | |
133 | /** @var string Global keyspace; used by makeGlobalKey() */ |
134 | protected const GLOBAL_KEYSPACE = 'global'; |
135 | /** @var string Precomputed global cache key prefix (needs no encoding) */ |
136 | protected const GLOBAL_PREFIX = 'global:'; |
137 | |
138 | /** @var int Item is a single cache key */ |
139 | protected const ARG0_KEY = 0; |
140 | /** @var int Item is an array of cache keys */ |
141 | protected const ARG0_KEYARR = 1; |
142 | /** @var int Item is an array indexed by cache keys */ |
143 | protected const ARG0_KEYMAP = 2; |
144 | /** @var int Item does not involve any keys */ |
145 | protected const ARG0_NONKEY = 3; |
146 | |
147 | /** @var int Item is an array indexed by cache keys */ |
148 | protected const RES_KEYMAP = 0; |
149 | /** @var int Item does not involve any keys */ |
150 | protected const RES_NONKEY = 1; |
151 | |
152 | /** |
153 | * @stable to call |
154 | * @param array $params Parameters include: |
155 | * - keyspace: Keyspace to use for keys in makeKey(). [Default: "local"] |
156 | * - asyncHandler: Callable to use for scheduling tasks after the web request ends. |
157 | * In CLI mode, it should run the task immediately. [Default: null] |
158 | * - stats: IStatsdDataFactory instance. [optional] |
159 | * - logger: Psr\Log\LoggerInterface instance. [optional] |
160 | * @phan-param array{keyspace?:string,logger?:Psr\Log\LoggerInterface,asyncHandler?:callable} $params |
161 | */ |
162 | public function __construct( array $params = [] ) { |
163 | $this->keyspace = $params['keyspace'] ?? 'local'; |
164 | $this->stats = $params['stats'] ?? StatsFactory::newNull(); |
165 | $this->setLogger( $params['logger'] ?? new NullLogger() ); |
166 | |
167 | $asyncHandler = $params['asyncHandler'] ?? null; |
168 | if ( is_callable( $asyncHandler ) ) { |
169 | $this->asyncHandler = $asyncHandler; |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * @param LoggerInterface $logger |
175 | * @return void |
176 | */ |
177 | public function setLogger( LoggerInterface $logger ) { |
178 | $this->logger = $logger; |
179 | } |
180 | |
181 | /** |
182 | * @since 1.35 |
183 | * @return LoggerInterface |
184 | */ |
185 | public function getLogger(): LoggerInterface { |
186 | return $this->logger; |
187 | } |
188 | |
189 | /** |
190 | * Get an item, regenerating and setting it if not found |
191 | * |
192 | * The callback can take $exptime as argument by reference and modify it. |
193 | * Nothing is stored nor deleted if the callback returns false. |
194 | * |
195 | * @param string $key |
196 | * @param int $exptime Time-to-live (seconds) |
197 | * @param callable $callback Callback that derives the new value |
198 | * @param int $flags Bitfield of BagOStuff::READ_* or BagOStuff::WRITE_* constants [optional] |
199 | * @return mixed The cached value if found or the result of $callback otherwise |
200 | * @since 1.27 |
201 | */ |
202 | final public function getWithSetCallback( $key, $exptime, $callback, $flags = 0 ) { |
203 | $value = $this->get( $key, $flags ); |
204 | |
205 | if ( $value === false ) { |
206 | $value = $callback( $exptime ); |
207 | if ( $value !== false && $exptime >= 0 ) { |
208 | $this->set( $key, $value, $exptime, $flags ); |
209 | } |
210 | } |
211 | |
212 | return $value; |
213 | } |
214 | |
215 | /** |
216 | * Get an item |
217 | * |
218 | * If the key includes a deterministic input hash (e.g. the key can only have |
219 | * the correct value) or complete staleness checks are handled by the caller |
220 | * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set. |
221 | * This lets tiered backends know they can safely upgrade a cached value to |
222 | * higher tiers using standard TTLs. |
223 | * |
224 | * @param string $key |
225 | * @param int $flags Bitfield of BagOStuff::READ_* constants [optional] |
226 | * @return mixed Returns false on failure or if the item does not exist |
227 | */ |
228 | abstract public function get( $key, $flags = 0 ); |
229 | |
230 | /** |
231 | * Set an item |
232 | * |
233 | * @param string $key |
234 | * @param mixed $value |
235 | * @param int $exptime Either an interval in seconds or a unix timestamp for expiry |
236 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants |
237 | * @return bool Success |
238 | */ |
239 | abstract public function set( $key, $value, $exptime = 0, $flags = 0 ); |
240 | |
241 | /** |
242 | * Delete an item if it exists |
243 | * |
244 | * For large values written using WRITE_ALLOW_SEGMENTS, this only deletes the main |
245 | * segment list key unless WRITE_PRUNE_SEGMENTS is in the flags. While deleting the segment |
246 | * list key has the effect of functionally deleting the key, it leaves unused blobs in cache. |
247 | * |
248 | * @param string $key |
249 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants |
250 | * @return bool Success (item deleted or not found) |
251 | */ |
252 | abstract public function delete( $key, $flags = 0 ); |
253 | |
254 | /** |
255 | * Insert an item if it does not already exist |
256 | * |
257 | * @param string $key |
258 | * @param mixed $value |
259 | * @param int $exptime |
260 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) |
261 | * @return bool Success (item created) |
262 | */ |
263 | abstract public function add( $key, $value, $exptime = 0, $flags = 0 ); |
264 | |
265 | /** |
266 | * Merge changes into the existing cache value (possibly creating a new one) |
267 | * |
268 | * The callback function returns the new value given the current value |
269 | * (which will be false if not present), and takes the arguments: |
270 | * (this BagOStuff, cache key, current value, TTL). |
271 | * The TTL parameter is reference set to $exptime. It can be overridden in the callback. |
272 | * Nothing is stored nor deleted if the callback returns false. |
273 | * |
274 | * @param string $key |
275 | * @param callable $callback Callback method to be executed |
276 | * @param int $exptime Either an interval in seconds or a unix timestamp for expiry |
277 | * @param int $attempts The amount of times to attempt a merge in case of failure |
278 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants |
279 | * @return bool Success |
280 | * @throws InvalidArgumentException |
281 | */ |
282 | abstract public function merge( |
283 | $key, |
284 | callable $callback, |
285 | $exptime = 0, |
286 | $attempts = 10, |
287 | $flags = 0 |
288 | ); |
289 | |
290 | /** |
291 | * Change the expiration on an item |
292 | * |
293 | * If an expiry in the past is given then the key will immediately be expired |
294 | * |
295 | * For large values written using WRITE_ALLOW_SEGMENTS, this only changes the TTL of the |
296 | * main segment list key. While lowering the TTL of the segment list key has the effect of |
297 | * functionally lowering the TTL of the key, it might leave unused blobs in cache for longer. |
298 | * Raising the TTL of such keys is not effective, since the expiration of a single segment |
299 | * key effectively expires the entire value. |
300 | * |
301 | * @param string $key |
302 | * @param int $exptime TTL or UNIX timestamp |
303 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) |
304 | * @return bool Success (item found and updated) |
305 | * @since 1.28 |
306 | */ |
307 | abstract public function changeTTL( $key, $exptime = 0, $flags = 0 ); |
308 | |
309 | /** |
310 | * Acquire an advisory lock on a key string, exclusive to the caller |
311 | * |
312 | * @param string $key |
313 | * @param int $timeout Lock wait timeout; 0 for non-blocking [optional] |
314 | * @param int $exptime Lock time-to-live in seconds; 1 day maximum [optional] |
315 | * @param string $rclass If this thread already holds the lock, and the lock was acquired |
316 | * using the same value for this parameter, then return true and use reference counting so |
317 | * that only the unlock() call from the outermost lock() caller actually releases the lock |
318 | * (note that only the outermost time-to-live is used) [optional] |
319 | * @return bool Success |
320 | */ |
321 | abstract public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ); |
322 | |
323 | /** |
324 | * Release an advisory lock on a key string |
325 | * |
326 | * @param string $key |
327 | * @return bool Success |
328 | */ |
329 | abstract public function unlock( $key ); |
330 | |
331 | /** |
332 | * Get a lightweight exclusive self-unlocking lock |
333 | * |
334 | * Note that the same lock cannot be acquired twice. |
335 | * |
336 | * This is useful for task de-duplication or to avoid obtrusive |
337 | * (though non-corrupting) DB errors like INSERT key conflicts |
338 | * or deadlocks when using LOCK IN SHARE MODE. |
339 | * |
340 | * @param string $key |
341 | * @param int $timeout Lock wait timeout; 0 for non-blocking [optional] |
342 | * @param int $exptime Lock time-to-live [optional]; 1 day maximum |
343 | * @param string $rclass Allow reentry if set and the current lock used this value |
344 | * @return ScopedCallback|null Returns null on failure |
345 | * @since 1.26 |
346 | */ |
347 | final public function getScopedLock( $key, $timeout = 6, $exptime = 30, $rclass = '' ) { |
348 | $exptime = min( $exptime ?: INF, self::TTL_DAY ); |
349 | |
350 | if ( !$this->lock( $key, $timeout, $exptime, $rclass ) ) { |
351 | return null; |
352 | } |
353 | |
354 | return new ScopedCallback( function () use ( $key ) { |
355 | $this->unlock( $key ); |
356 | } ); |
357 | } |
358 | |
359 | /** |
360 | * Delete all objects expiring before a certain date |
361 | * |
362 | * @param string|int $timestamp The reference date in MW or TS_UNIX format |
363 | * @param callable|null $progress Optional, a function which will be called |
364 | * regularly during long-running operations with the percentage progress |
365 | * as the first parameter. [optional] |
366 | * @param int|float $limit Maximum number of keys to delete [default: INF] |
367 | * @param string|null $tag Tag to purge a single shard only. |
368 | * This is only supported when server tags are used in configuration. |
369 | * @return bool Success; false if unimplemented |
370 | */ |
371 | abstract public function deleteObjectsExpiringBefore( |
372 | $timestamp, |
373 | callable $progress = null, |
374 | $limit = INF, |
375 | string $tag = null |
376 | ); |
377 | |
378 | /** |
379 | * Get a batch of items |
380 | * |
381 | * @param string[] $keys List of keys |
382 | * @param int $flags Bitfield; supports READ_LATEST [optional] |
383 | * @return mixed[] Map of (key => value) for existing keys |
384 | */ |
385 | abstract public function getMulti( array $keys, $flags = 0 ); |
386 | |
387 | /** |
388 | * Set a batch of items |
389 | * |
390 | * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O |
391 | * |
392 | * WRITE_BACKGROUND can be used for bulk insertion where the response is not vital |
393 | * |
394 | * @param mixed[] $valueByKey Map of (key => value) |
395 | * @param int $exptime Either an interval in seconds or a unix timestamp for expiry |
396 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) |
397 | * @return bool Success |
398 | * @since 1.24 |
399 | */ |
400 | abstract public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ); |
401 | |
402 | /** |
403 | * Delete a batch of items |
404 | * |
405 | * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O |
406 | * |
407 | * WRITE_BACKGROUND can be used for bulk deletion where the response is not vital |
408 | * |
409 | * @param string[] $keys List of keys |
410 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants |
411 | * @return bool Success (items deleted and/or not found) |
412 | * @since 1.33 |
413 | */ |
414 | abstract public function deleteMulti( array $keys, $flags = 0 ); |
415 | |
416 | /** |
417 | * Change the expiration of multiple items |
418 | * |
419 | * @see BagOStuff::changeTTL() |
420 | * |
421 | * @param string[] $keys List of keys |
422 | * @param int $exptime TTL or UNIX timestamp |
423 | * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) |
424 | * @return bool Success (all items found and updated) |
425 | * @since 1.34 |
426 | */ |
427 | abstract public function changeTTLMulti( array $keys, $exptime, $flags = 0 ); |
428 | |
429 | /** |
430 | * Increase the value of the given key (no TTL change) if it exists or create it otherwise |
431 | * |
432 | * This will create the key with the value $init and TTL $exptime instead if not present. |
433 | * Callers should make sure that both ($init - $step) and $exptime are invariants for all |
434 | * operations to any given key. The value of $init should be at least that of $step. |
435 | * |
436 | * The new value is returned, except if the WRITE_BACKGROUND flag is given, in which case |
437 | * the handler may choose to return true to indicate that the operation has been dispatched. |
438 | * |
439 | * @param string $key Key built via makeKey() or makeGlobalKey() |
440 | * @param int $exptime Time-to-live (in seconds) or a UNIX timestamp expiration |
441 | * @param int $step Amount to increase the key value by [default: 1] |
442 | * @param int|null $init Value to initialize the key to if it does not exist [default: $step] |
443 | * @param int $flags Bit field of class WRITE_* constants [optional] |
444 | * @return int|bool New value (or true if asynchronous) on success; false on failure |
445 | * @since 1.24 |
446 | */ |
447 | abstract public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ); |
448 | |
449 | /** |
450 | * Get a "watch point" token that can be used to get the "last error" to occur after now |
451 | * |
452 | * @return int A token to that can be used with getLastError() |
453 | * @since 1.38 |
454 | */ |
455 | public function watchErrors() { |
456 | return self::$nextErrorMonitorId++; |
457 | } |
458 | |
459 | /** |
460 | * Get the "last error" registry |
461 | * |
462 | * The method should be invoked by a caller as part of the following pattern: |
463 | * - The caller invokes watchErrors() to get a "since token" |
464 | * - The caller invokes a sequence of cache operation methods |
465 | * - The caller invokes getLastError() with the "since token" |
466 | * |
467 | * External callers can also invoke this method as part of the following pattern: |
468 | * - The caller invokes clearLastError() |
469 | * - The caller invokes a sequence of cache operation methods |
470 | * - The caller invokes getLastError() |
471 | * |
472 | * @param int $watchPoint Only consider errors from after this "watch point" [optional] |
473 | * @return int BagOStuff:ERR_* constant for the "last error" registry |
474 | * @note Parameters added in 1.38: $watchPoint |
475 | * @since 1.23 |
476 | */ |
477 | public function getLastError( $watchPoint = 0 ) { |
478 | return ( $this->lastErrorId > $watchPoint ) ? $this->lastError : self::ERR_NONE; |
479 | } |
480 | |
481 | /** |
482 | * Clear the "last error" registry |
483 | * |
484 | * @since 1.23 |
485 | * @deprecated Since 1.38 |
486 | */ |
487 | public function clearLastError() { |
488 | $this->lastError = self::ERR_NONE; |
489 | } |
490 | |
491 | /** |
492 | * Set the "last error" registry due to a problem encountered during an attempted operation |
493 | * |
494 | * @param int $error BagOStuff:ERR_* constant |
495 | * @since 1.23 |
496 | */ |
497 | protected function setLastError( $error ) { |
498 | $this->lastError = $error; |
499 | $this->lastErrorId = self::$nextErrorMonitorId++; |
500 | } |
501 | |
502 | /** |
503 | * Make a cache key from the given components, in the "global" keyspace |
504 | * |
505 | * Global keys are shared with and visible to all sites hosted in the same |
506 | * infrastructure (e.g. cross-wiki within the same wiki farm). Others sites |
507 | * may read the stored value from their requests, and they must be able to |
508 | * correctly compute new values from their own request context. |
509 | * |
510 | * @see BagOStuff::makeKeyInternal |
511 | * @since 1.27 |
512 | * @param string $keygroup Key group component, should be under 48 characters. |
513 | * @param string|int ...$components Additional, ordered, key components for entity IDs |
514 | * @return string Colon-separated, keyspace-prepended, ordered list of encoded components |
515 | */ |
516 | public function makeGlobalKey( $keygroup, ...$components ) { |
517 | return $this->makeKeyInternal( self::GLOBAL_KEYSPACE, func_get_args() ); |
518 | } |
519 | |
520 | /** |
521 | * Make a cache key from the given components, in the default keyspace |
522 | * |
523 | * The default keyspace is unique to a given site. Subsequent web requests |
524 | * to the same site (e.g. local wiki, or same domain name) will interact |
525 | * with the same keyspace. |
526 | * |
527 | * Requests to other sites hosted on the same infrastructure (e.g. cross-wiki |
528 | * or cross-domain), have their own keyspace that naturally avoids conflicts. |
529 | * |
530 | * As caller you are responsible for: |
531 | * - Limit the key group (first component) to 48 characters |
532 | * |
533 | * Internally, the colon is used as delimiter (":"), and this is |
534 | * automatically escaped in supplied components to avoid ambiguity or |
535 | * key conflicts. BagOStuff subclasses are responsible for applying any |
536 | * additional escaping or limits as-needed before sending commands over |
537 | * the network. |
538 | * |
539 | * @see BagOStuff::makeKeyInternal |
540 | * @since 1.27 |
541 | * @param string $keygroup Key group component, should be under 48 characters. |
542 | * @param string|int ...$components Additional, ordered, key components for entity IDs |
543 | * @return string Colon-separated, keyspace-prepended, ordered list of encoded components |
544 | */ |
545 | public function makeKey( $keygroup, ...$components ) { |
546 | return $this->makeKeyInternal( $this->keyspace, func_get_args() ); |
547 | } |
548 | |
549 | /** |
550 | * Check whether a cache key is in the global keyspace |
551 | * |
552 | * @param string $key |
553 | * @return bool |
554 | * @since 1.35 |
555 | */ |
556 | public function isKeyGlobal( $key ) { |
557 | return str_starts_with( $key, self::GLOBAL_PREFIX ); |
558 | } |
559 | |
560 | /** |
561 | * @param int $flag BagOStuff::ATTR_* constant |
562 | * @return int BagOStuff:QOS_* constant |
563 | * @since 1.28 |
564 | */ |
565 | public function getQoS( $flag ) { |
566 | return $this->attrMap[$flag] ?? self::QOS_UNKNOWN; |
567 | } |
568 | |
569 | /** |
570 | * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit) |
571 | * @since 1.34 |
572 | */ |
573 | public function getSegmentationSize() { |
574 | return INF; |
575 | } |
576 | |
577 | /** |
578 | * @return int|float Maximum total segmented object size in bytes (INF for no limit) |
579 | * @since 1.34 |
580 | */ |
581 | public function getSegmentedValueMaxSize() { |
582 | return INF; |
583 | } |
584 | |
585 | /** |
586 | * @param int $field |
587 | * @param int $flags |
588 | * @return bool |
589 | * @since 1.34 |
590 | */ |
591 | final protected function fieldHasFlags( $field, $flags ) { |
592 | return ( ( $field & $flags ) === $flags ); |
593 | } |
594 | |
595 | /** |
596 | * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map |
597 | * |
598 | * @param BagOStuff[] $bags |
599 | * @return int[] Resulting flag map (class ATTR_* constant => class QOS_* constant) |
600 | */ |
601 | final protected function mergeFlagMaps( array $bags ) { |
602 | $map = []; |
603 | foreach ( $bags as $bag ) { |
604 | foreach ( $bag->attrMap as $attr => $rank ) { |
605 | if ( isset( $map[$attr] ) ) { |
606 | $map[$attr] = min( $map[$attr], $rank ); |
607 | } else { |
608 | $map[$attr] = $rank; |
609 | } |
610 | } |
611 | } |
612 | |
613 | return $map; |
614 | } |
615 | |
616 | /** |
617 | * Stage a set of new key values for storage and estimate the amount of bytes needed |
618 | * |
619 | * @param array $valueByKey Map of (cache key => PHP variable value to serialize) |
620 | * @return int[]|null[] Corresponding list of size estimates (null for invalid values) |
621 | * @since 1.35 |
622 | * @deprecated Since 1.40, Hard deprecated since 1.42. |
623 | */ |
624 | public function setNewPreparedValues( array $valueByKey ) { |
625 | wfDeprecated( __METHOD__, '1.40' ); |
626 | $sizes = []; |
627 | foreach ( $valueByKey as $value ) { |
628 | // Roughly estimate the size of the value once serialized. This does not account |
629 | // for the use of non-PHP serialization (e.g. igbinary/msgpack/json), compression |
630 | // (e.g. gzip/lzma), nor protocol/storage metadata. |
631 | if ( is_string( $value ) ) { |
632 | // E.g. "<type><delim1><quote><value><quote><delim2>" |
633 | $size = strlen( $value ) + 5; |
634 | } else { |
635 | $size = strlen( serialize( $value ) ); |
636 | } |
637 | $sizes[] = $size; |
638 | } |
639 | |
640 | return $sizes; |
641 | } |
642 | |
643 | /** |
644 | * Make a cache key for the given keyspace and components |
645 | * |
646 | * Subclasses may override this method in order to apply different escaping, |
647 | * or to deal with size constraints (such as MemcachedBagOStuff). For example |
648 | * by converting long components into hashes. |
649 | * |
650 | * If you override this method, you MUST override ::requireConvertGenericKey() |
651 | * to return true. This ensures that wrapping classes (e.g. MultiWriteBagOStuff) |
652 | * know to re-encode keys before calling read/write methods. See also ::proxyCall(). |
653 | * |
654 | * @see BagOStuff::proxyCall |
655 | * @since 1.27 |
656 | * @param string $keyspace |
657 | * @param string[]|int[] $components Key group and other components |
658 | * @return string |
659 | */ |
660 | protected function makeKeyInternal( $keyspace, $components ) { |
661 | if ( count( $components ) < 1 ) { |
662 | throw new InvalidArgumentException( "Missing key group" ); |
663 | } |
664 | |
665 | $key = $keyspace; |
666 | foreach ( $components as $i => $component ) { |
667 | // Escape delimiter (":") and escape ("%") characters |
668 | $key .= ':' . strtr( $component, [ '%' => '%25', ':' => '%3A' ] ); |
669 | } |
670 | return $key; |
671 | } |
672 | |
673 | /** |
674 | * Whether ::proxyCall() must re-encode cache keys before calling read/write methods. |
675 | * |
676 | * @stable to override |
677 | * @see BagOStuff::makeKeyInternal |
678 | * @see BagOStuff::proxyCall |
679 | * @since 1.41 |
680 | * @return bool |
681 | */ |
682 | protected function requireConvertGenericKey(): bool { |
683 | return false; |
684 | } |
685 | |
686 | /** |
687 | * Convert a key from BagOStuff::makeKeyInternal into one for the current subclass |
688 | * |
689 | * @see BagOStuff::proxyCall |
690 | * @param string $key Result from BagOStuff::makeKeyInternal |
691 | * @return string Result from current subclass override of BagOStuff::makeKeyInternal |
692 | */ |
693 | private function convertGenericKey( $key ) { |
694 | if ( !$this->requireConvertGenericKey() ) { |
695 | // If subclass doesn't overwrite makeKeyInternal, no re-encoding is needed. |
696 | return $key; |
697 | } |
698 | |
699 | // Extract the components from a "generic" key formatted by BagOStuff::makeKeyInternal() |
700 | // Note that the order of each corresponding search/replace pair matters! |
701 | $components = str_replace( [ '%3A', '%25' ], [ ':', '%' ], explode( ':', $key ) ); |
702 | if ( count( $components ) < 2 ) { |
703 | // Legacy key, not even formatted by makeKey()/makeGlobalKey(). Keep as-is. |
704 | return $key; |
705 | } |
706 | |
707 | $keyspace = array_shift( $components ); |
708 | |
709 | return $this->makeKeyInternal( $keyspace, $components ); |
710 | } |
711 | |
712 | /** |
713 | * Call a method on behalf of wrapper BagOStuff instance |
714 | * |
715 | * The "wrapper" BagOStuff subclass that calls proxyCall() MUST NOT override |
716 | * the default makeKeyInternal() implementation, because proxyCall() needs |
717 | * to turn the "generic" key back into an array, and re-format it according |
718 | * to the backend-specific BagOStuff::makeKey implementation. |
719 | * |
720 | * For example, when using MultiWriteBagOStuff with Memcached as a backend, |
721 | * writes will go via MemcachedBagOStuff::proxyCall(), which then reformats |
722 | * the "generic" result of BagOStuff::makeKey (called as MultiWriteBagOStuff::makeKey) |
723 | * using MemcachedBagOStuff::makeKeyInternal. |
724 | * |
725 | * @param string $method Name of a non-final public method that reads/changes keys |
726 | * @param int $arg0Sig BagOStuff::ARG0_* constant describing argument 0 |
727 | * @param int $resSig BagOStuff::RES_* constant describing the return value |
728 | * @param array $genericArgs Method arguments passed to the wrapper instance |
729 | * @param BagOStuff $wrapper The wrapper BagOStuff instance using this result |
730 | * @return mixed Method result with any keys remapped to "generic" keys |
731 | */ |
732 | protected function proxyCall( |
733 | string $method, |
734 | int $arg0Sig, |
735 | int $resSig, |
736 | array $genericArgs, |
737 | BagOStuff $wrapper |
738 | ) { |
739 | // Get the corresponding store-specific cache keys... |
740 | $storeArgs = $genericArgs; |
741 | switch ( $arg0Sig ) { |
742 | case self::ARG0_KEY: |
743 | $storeArgs[0] = $this->convertGenericKey( $genericArgs[0] ); |
744 | break; |
745 | case self::ARG0_KEYARR: |
746 | foreach ( $genericArgs[0] as $i => $genericKey ) { |
747 | $storeArgs[0][$i] = $this->convertGenericKey( $genericKey ); |
748 | } |
749 | break; |
750 | case self::ARG0_KEYMAP: |
751 | $storeArgs[0] = []; |
752 | foreach ( $genericArgs[0] as $genericKey => $v ) { |
753 | $storeArgs[0][$this->convertGenericKey( $genericKey )] = $v; |
754 | } |
755 | break; |
756 | } |
757 | |
758 | // Result of invoking the method with the corresponding store-specific cache keys |
759 | $watchPoint = $this->watchErrors(); |
760 | $storeRes = $this->$method( ...$storeArgs ); |
761 | $lastError = $this->getLastError( $watchPoint ); |
762 | if ( $lastError !== self::ERR_NONE ) { |
763 | $wrapper->setLastError( $lastError ); |
764 | } |
765 | |
766 | // Convert any store-specific cache keys in the result back to generic cache keys |
767 | if ( $resSig === self::RES_KEYMAP ) { |
768 | // Map of (store-specific cache key => generic cache key) |
769 | $genericKeyByStoreKey = array_combine( $storeArgs[0], $genericArgs[0] ); |
770 | |
771 | $genericRes = []; |
772 | foreach ( $storeRes as $storeKey => $value ) { |
773 | $genericRes[$genericKeyByStoreKey[$storeKey]] = $value; |
774 | } |
775 | } else { |
776 | $genericRes = $storeRes; |
777 | } |
778 | |
779 | return $genericRes; |
780 | } |
781 | |
782 | /** |
783 | * @internal For testing only |
784 | * @return float UNIX timestamp |
785 | * @codeCoverageIgnore |
786 | */ |
787 | public function getCurrentTime() { |
788 | return $this->wallClockOverride ?: microtime( true ); |
789 | } |
790 | |
791 | /** |
792 | * @internal For testing only |
793 | * @param float|null &$time Mock UNIX timestamp |
794 | * @codeCoverageIgnore |
795 | */ |
796 | public function setMockTime( &$time ) { |
797 | $this->wallClockOverride =& $time; |
798 | } |
799 | } |