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