BagOStuff::getLastError()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright © 2003-2004 Brion Vibber <[email protected]>
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\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
33
use Wikimedia\WaitConditionLoop;
34
35
/**
36
 * interface is intended to be more or less compatible with
37
 * the PHP memcached client.
38
 *
39
 * backends for local hash array and SQL table included:
40
 * @code
41
 *   $bag = new HashBagOStuff();
42
 *   $bag = new SqlBagOStuff(); # connect to db first
43
 * @endcode
44
 *
45
 * @ingroup Cache
46
 */
47
abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
48
	/** @var array[] Lock tracking */
49
	protected $locks = [];
50
	/** @var integer ERR_* class constant */
51
	protected $lastError = self::ERR_NONE;
52
	/** @var string */
53
	protected $keyspace = 'local';
54
	/** @var LoggerInterface */
55
	protected $logger;
56
	/** @var callback|null */
57
	protected $asyncHandler;
58
	/** @var integer Seconds */
59
	protected $syncTimeout;
60
61
	/** @var bool */
62
	private $debugMode = false;
63
	/** @var array */
64
	private $duplicateKeyLookups = [];
65
	/** @var bool */
66
	private $reportDupes = false;
67
	/** @var bool */
68
	private $dupeTrackScheduled = false;
69
70
	/** @var callable[] */
71
	protected $busyCallbacks = [];
72
73
	/** @var integer[] Map of (ATTR_* class constant => QOS_* class constant) */
74
	protected $attrMap = [];
75
76
	/** Possible values for getLastError() */
77
	const ERR_NONE = 0; // no error
78
	const ERR_NO_RESPONSE = 1; // no response
79
	const ERR_UNREACHABLE = 2; // can't connect
80
	const ERR_UNEXPECTED = 3; // response gave some error
81
82
	/** Bitfield constants for get()/getMulti() */
83
	const READ_LATEST = 1; // use latest data for replicated stores
84
	const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
85
	/** Bitfield constants for set()/merge() */
86
	const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
87
	const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
88
89
	/**
90
	 * $params include:
91
	 *   - logger: Psr\Log\LoggerInterface instance
92
	 *   - keyspace: Default keyspace for $this->makeKey()
93
	 *   - asyncHandler: Callable to use for scheduling tasks after the web request ends.
94
	 *      In CLI mode, it should run the task immediately.
95
	 *   - reportDupes: Whether to emit warning log messages for all keys that were
96
	 *      requested more than once (requires an asyncHandler).
97
	 *   - syncTimeout: How long to wait with WRITE_SYNC in seconds.
98
	 * @param array $params
99
	 */
100
	public function __construct( array $params = [] ) {
101
		if ( isset( $params['logger'] ) ) {
102
			$this->setLogger( $params['logger'] );
103
		} else {
104
			$this->setLogger( new NullLogger() );
105
		}
106
107
		if ( isset( $params['keyspace'] ) ) {
108
			$this->keyspace = $params['keyspace'];
109
		}
110
111
		$this->asyncHandler = isset( $params['asyncHandler'] )
112
			? $params['asyncHandler']
113
			: null;
114
115
		if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
116
			$this->reportDupes = true;
117
		}
118
119
		$this->syncTimeout = isset( $params['syncTimeout'] ) ? $params['syncTimeout'] : 3;
120
	}
121
122
	/**
123
	 * @param LoggerInterface $logger
124
	 * @return null
125
	 */
126
	public function setLogger( LoggerInterface $logger ) {
127
		$this->logger = $logger;
128
	}
129
130
	/**
131
	 * @param bool $bool
132
	 */
133
	public function setDebug( $bool ) {
134
		$this->debugMode = $bool;
135
	}
136
137
	/**
138
	 * Get an item with the given key, regenerating and setting it if not found
139
	 *
140
	 * If the callback returns false, then nothing is stored.
141
	 *
142
	 * @param string $key
143
	 * @param int $ttl Time-to-live (seconds)
144
	 * @param callable $callback Callback that derives the new value
145
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
146
	 * @return mixed The cached value if found or the result of $callback otherwise
147
	 * @since 1.27
148
	 */
149
	final public function getWithSetCallback( $key, $ttl, $callback, $flags = 0 ) {
150
		$value = $this->get( $key, $flags );
151
152
		if ( $value === false ) {
153
			if ( !is_callable( $callback ) ) {
154
				throw new InvalidArgumentException( "Invalid cache miss callback provided." );
155
			}
156
			$value = call_user_func( $callback );
157
			if ( $value !== false ) {
158
				$this->set( $key, $value, $ttl );
159
			}
160
		}
161
162
		return $value;
163
	}
164
165
	/**
166
	 * Get an item with the given key
167
	 *
168
	 * If the key includes a determistic input hash (e.g. the key can only have
169
	 * the correct value) or complete staleness checks are handled by the caller
170
	 * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
171
	 * This lets tiered backends know they can safely upgrade a cached value to
172
	 * higher tiers using standard TTLs.
173
	 *
174
	 * @param string $key
175
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
176
	 * @param integer $oldFlags [unused]
177
	 * @return mixed Returns false on failure and if the item does not exist
178
	 */
179
	public function get( $key, $flags = 0, $oldFlags = null ) {
180
		// B/C for ( $key, &$casToken = null, $flags = 0 )
181
		$flags = is_int( $oldFlags ) ? $oldFlags : $flags;
182
183
		$this->trackDuplicateKeys( $key );
184
185
		return $this->doGet( $key, $flags );
186
	}
187
188
	/**
189
	 * Track the number of times that a given key has been used.
190
	 * @param string $key
191
	 */
192
	private function trackDuplicateKeys( $key ) {
193
		if ( !$this->reportDupes ) {
194
			return;
195
		}
196
197
		if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
198
			// Track that we have seen this key. This N-1 counting style allows
199
			// easy filtering with array_filter() later.
200
			$this->duplicateKeyLookups[$key] = 0;
201
		} else {
202
			$this->duplicateKeyLookups[$key] += 1;
203
204
			if ( $this->dupeTrackScheduled === false ) {
205
				$this->dupeTrackScheduled = true;
206
				// Schedule a callback that logs keys processed more than once by get().
207
				call_user_func( $this->asyncHandler, function () {
208
					$dups = array_filter( $this->duplicateKeyLookups );
209
					foreach ( $dups as $key => $count ) {
210
						$this->logger->warning(
211
							'Duplicate get(): "{key}" fetched {count} times',
212
							// Count is N-1 of the actual lookup count
213
							[ 'key' => $key, 'count' => $count + 1, ]
214
						);
215
					}
216
				} );
217
			}
218
		}
219
	}
220
221
	/**
222
	 * @param string $key
223
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
224
	 * @return mixed Returns false on failure and if the item does not exist
225
	 */
226
	abstract protected function doGet( $key, $flags = 0 );
227
228
	/**
229
	 * @note: This method is only needed if merge() uses mergeViaCas()
230
	 *
231
	 * @param string $key
232
	 * @param mixed $casToken
233
	 * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
234
	 * @return mixed Returns false on failure and if the item does not exist
235
	 * @throws Exception
236
	 */
237
	protected function getWithToken( $key, &$casToken, $flags = 0 ) {
238
		throw new Exception( __METHOD__ . ' not implemented.' );
239
	}
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
	 * @return bool Success
249
	 */
250
	abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
251
252
	/**
253
	 * Delete an item
254
	 *
255
	 * @param string $key
256
	 * @return bool True if the item was deleted or not found, false on failure
257
	 */
258
	abstract public function delete( $key );
259
260
	/**
261
	 * Merge changes into the existing cache value (possibly creating a new one)
262
	 *
263
	 * The callback function returns the new value given the current value
264
	 * (which will be false if not present), and takes the arguments:
265
	 * (this BagOStuff, cache key, current value, TTL).
266
	 * The TTL parameter is reference set to $exptime. It can be overriden in the callback.
267
	 *
268
	 * @param string $key
269
	 * @param callable $callback Callback method to be executed
270
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
271
	 * @param int $attempts The amount of times to attempt a merge in case of failure
272
	 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
273
	 * @return bool Success
274
	 * @throws InvalidArgumentException
275
	 */
276
	public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
277
		return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
278
	}
279
280
	/**
281
	 * @see BagOStuff::merge()
282
	 *
283
	 * @param string $key
284
	 * @param callable $callback Callback method to be executed
285
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
286
	 * @param int $attempts The amount of times to attempt a merge in case of failure
287
	 * @return bool Success
288
	 */
289
	protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
290
		do {
291
			$this->clearLastError();
292
			$reportDupes = $this->reportDupes;
293
			$this->reportDupes = false;
294
			$casToken = null; // passed by reference
295
			$currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
296
			$this->reportDupes = $reportDupes;
297
298
			if ( $this->getLastError() ) {
299
				return false; // don't spam retries (retry only on races)
300
			}
301
302
			// Derive the new value from the old value
303
			$value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
304
305
			$this->clearLastError();
306 View Code Duplication
			if ( $value === false ) {
307
				$success = true; // do nothing
308
			} elseif ( $currentValue === false ) {
309
				// Try to create the key, failing if it gets created in the meantime
310
				$success = $this->add( $key, $value, $exptime );
311
			} else {
312
				// Try to update the key, failing if it gets changed in the meantime
313
				$success = $this->cas( $casToken, $key, $value, $exptime );
314
			}
315
			if ( $this->getLastError() ) {
316
				return false; // IO error; don't spam retries
317
			}
318
		} while ( !$success && --$attempts );
319
320
		return $success;
321
	}
322
323
	/**
324
	 * Check and set an item
325
	 *
326
	 * @param mixed $casToken
327
	 * @param string $key
328
	 * @param mixed $value
329
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
330
	 * @return bool Success
331
	 * @throws Exception
332
	 */
333
	protected function cas( $casToken, $key, $value, $exptime = 0 ) {
334
		throw new Exception( "CAS is not implemented in " . __CLASS__ );
335
	}
336
337
	/**
338
	 * @see BagOStuff::merge()
339
	 *
340
	 * @param string $key
341
	 * @param callable $callback Callback method to be executed
342
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
343
	 * @param int $attempts The amount of times to attempt a merge in case of failure
344
	 * @param int $flags Bitfield of BagOStuff::WRITE_* constants
345
	 * @return bool Success
346
	 */
347
	protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
348
		if ( !$this->lock( $key, 6 ) ) {
349
			return false;
350
		}
351
352
		$this->clearLastError();
353
		$reportDupes = $this->reportDupes;
354
		$this->reportDupes = false;
355
		$currentValue = $this->get( $key, self::READ_LATEST );
356
		$this->reportDupes = $reportDupes;
357
358
		if ( $this->getLastError() ) {
359
			$success = false;
360 View Code Duplication
		} else {
361
			// Derive the new value from the old value
362
			$value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
363
			if ( $value === false ) {
364
				$success = true; // do nothing
365
			} else {
366
				$success = $this->set( $key, $value, $exptime, $flags ); // set the new value
367
			}
368
		}
369
370
		if ( !$this->unlock( $key ) ) {
371
			// this should never happen
372
			trigger_error( "Could not release lock for key '$key'." );
373
		}
374
375
		return $success;
376
	}
377
378
	/**
379
	 * Reset the TTL on a key if it exists
380
	 *
381
	 * @param string $key
382
	 * @param int $expiry
383
	 * @return bool Success Returns false if there is no key
384
	 * @since 1.28
385
	 */
386
	public function changeTTL( $key, $expiry = 0 ) {
387
		$value = $this->get( $key );
388
389
		return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
390
	}
391
392
	/**
393
	 * Acquire an advisory lock on a key string
394
	 *
395
	 * Note that if reentry is enabled, duplicate calls ignore $expiry
396
	 *
397
	 * @param string $key
398
	 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
399
	 * @param int $expiry Lock expiry [optional]; 1 day maximum
400
	 * @param string $rclass Allow reentry if set and the current lock used this value
401
	 * @return bool Success
402
	 */
403
	public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
404
		// Avoid deadlocks and allow lock reentry if specified
405
		if ( isset( $this->locks[$key] ) ) {
406
			if ( $rclass != '' && $this->locks[$key]['class'] === $rclass ) {
407
				++$this->locks[$key]['depth'];
408
				return true;
409
			} else {
410
				return false;
411
			}
412
		}
413
414
		$expiry = min( $expiry ?: INF, self::TTL_DAY );
415
		$loop = new WaitConditionLoop(
416
			function () use ( $key, $timeout, $expiry ) {
417
				$this->clearLastError();
418
				if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
419
					return true; // locked!
420
				} elseif ( $this->getLastError() ) {
421
					return WaitConditionLoop::CONDITION_ABORTED; // network partition?
422
				}
423
424
				return WaitConditionLoop::CONDITION_CONTINUE;
425
			},
426
			$timeout
427
		);
428
429
		$locked = ( $loop->invoke() === $loop::CONDITION_REACHED );
430
		if ( $locked ) {
431
			$this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
432
		}
433
434
		return $locked;
435
	}
436
437
	/**
438
	 * Release an advisory lock on a key string
439
	 *
440
	 * @param string $key
441
	 * @return bool Success
442
	 */
443
	public function unlock( $key ) {
444
		if ( isset( $this->locks[$key] ) && --$this->locks[$key]['depth'] <= 0 ) {
445
			unset( $this->locks[$key] );
446
447
			return $this->delete( "{$key}:lock" );
448
		}
449
450
		return true;
451
	}
452
453
	/**
454
	 * Get a lightweight exclusive self-unlocking lock
455
	 *
456
	 * Note that the same lock cannot be acquired twice.
457
	 *
458
	 * This is useful for task de-duplication or to avoid obtrusive
459
	 * (though non-corrupting) DB errors like INSERT key conflicts
460
	 * or deadlocks when using LOCK IN SHARE MODE.
461
	 *
462
	 * @param string $key
463
	 * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
464
	 * @param int $expiry Lock expiry [optional]; 1 day maximum
465
	 * @param string $rclass Allow reentry if set and the current lock used this value
466
	 * @return ScopedCallback|null Returns null on failure
467
	 * @since 1.26
468
	 */
469
	final public function getScopedLock( $key, $timeout = 6, $expiry = 30, $rclass = '' ) {
470
		$expiry = min( $expiry ?: INF, self::TTL_DAY );
471
472
		if ( !$this->lock( $key, $timeout, $expiry, $rclass ) ) {
473
			return null;
474
		}
475
476
		$lSince = microtime( true ); // lock timestamp
477
478
		return new ScopedCallback( function() use ( $key, $lSince, $expiry ) {
479
			$latency = .050; // latency skew (err towards keeping lock present)
480
			$age = ( microtime( true ) - $lSince + $latency );
481
			if ( ( $age + $latency ) >= $expiry ) {
482
				$this->logger->warning( "Lock for $key held too long ($age sec)." );
483
				return; // expired; it's not "safe" to delete the key
484
			}
485
			$this->unlock( $key );
486
		} );
487
	}
488
489
	/**
490
	 * Delete all objects expiring before a certain date.
491
	 * @param string $date The reference date in MW format
492
	 * @param callable|bool $progressCallback Optional, a function which will be called
493
	 *     regularly during long-running operations with the percentage progress
494
	 *     as the first parameter.
495
	 *
496
	 * @return bool Success, false if unimplemented
497
	 */
498
	public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
499
		// stub
500
		return false;
501
	}
502
503
	/**
504
	 * Get an associative array containing the item for each of the keys that have items.
505
	 * @param array $keys List of strings
506
	 * @param integer $flags Bitfield; supports READ_LATEST [optional]
507
	 * @return array
508
	 */
509
	public function getMulti( array $keys, $flags = 0 ) {
510
		$res = [];
511
		foreach ( $keys as $key ) {
512
			$val = $this->get( $key );
513
			if ( $val !== false ) {
514
				$res[$key] = $val;
515
			}
516
		}
517
		return $res;
518
	}
519
520
	/**
521
	 * Batch insertion
522
	 * @param array $data $key => $value assoc array
523
	 * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
524
	 * @return bool Success
525
	 * @since 1.24
526
	 */
527
	public function setMulti( array $data, $exptime = 0 ) {
528
		$res = true;
529
		foreach ( $data as $key => $value ) {
530
			if ( !$this->set( $key, $value, $exptime ) ) {
531
				$res = false;
532
			}
533
		}
534
		return $res;
535
	}
536
537
	/**
538
	 * @param string $key
539
	 * @param mixed $value
540
	 * @param int $exptime
541
	 * @return bool Success
542
	 */
543
	public function add( $key, $value, $exptime = 0 ) {
544
		if ( $this->get( $key ) === false ) {
545
			return $this->set( $key, $value, $exptime );
546
		}
547
		return false; // key already set
548
	}
549
550
	/**
551
	 * Increase stored value of $key by $value while preserving its TTL
552
	 * @param string $key Key to increase
553
	 * @param int $value Value to add to $key (Default 1)
554
	 * @return int|bool New value or false on failure
555
	 */
556
	public function incr( $key, $value = 1 ) {
557
		if ( !$this->lock( $key ) ) {
558
			return false;
559
		}
560
		$n = $this->get( $key );
561
		if ( $this->isInteger( $n ) ) { // key exists?
562
			$n += intval( $value );
563
			$this->set( $key, max( 0, $n ) ); // exptime?
564
		} else {
565
			$n = false;
566
		}
567
		$this->unlock( $key );
568
569
		return $n;
570
	}
571
572
	/**
573
	 * Decrease stored value of $key by $value while preserving its TTL
574
	 * @param string $key
575
	 * @param int $value
576
	 * @return int|bool New value or false on failure
577
	 */
578
	public function decr( $key, $value = 1 ) {
579
		return $this->incr( $key, - $value );
580
	}
581
582
	/**
583
	 * Increase stored value of $key by $value while preserving its TTL
584
	 *
585
	 * This will create the key with value $init and TTL $ttl instead if not present
586
	 *
587
	 * @param string $key
588
	 * @param int $ttl
589
	 * @param int $value
590
	 * @param int $init
591
	 * @return int|bool New value or false on failure
592
	 * @since 1.24
593
	 */
594
	public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
595
		$newValue = $this->incr( $key, $value );
596
		if ( $newValue === false ) {
597
			// No key set; initialize
598
			$newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
599
		}
600
		if ( $newValue === false ) {
601
			// Raced out initializing; increment
602
			$newValue = $this->incr( $key, $value );
603
		}
604
605
		return $newValue;
606
	}
607
608
	/**
609
	 * Get the "last error" registered; clearLastError() should be called manually
610
	 * @return int ERR_* constant for the "last error" registry
611
	 * @since 1.23
612
	 */
613
	public function getLastError() {
614
		return $this->lastError;
615
	}
616
617
	/**
618
	 * Clear the "last error" registry
619
	 * @since 1.23
620
	 */
621
	public function clearLastError() {
622
		$this->lastError = self::ERR_NONE;
623
	}
624
625
	/**
626
	 * Set the "last error" registry
627
	 * @param int $err ERR_* constant
628
	 * @since 1.23
629
	 */
630
	protected function setLastError( $err ) {
631
		$this->lastError = $err;
632
	}
633
634
	/**
635
	 * Let a callback be run to avoid wasting time on special blocking calls
636
	 *
637
	 * The callbacks may or may not be called ever, in any particular order.
638
	 * They are likely to be invoked when something WRITE_SYNC is used used.
639
	 * They should follow a caching pattern as shown below, so that any code
640
	 * using the word will get it's result no matter what happens.
641
	 * @code
642
	 *     $result = null;
643
	 *     $workCallback = function () use ( &$result ) {
644
	 *         if ( !$result ) {
645
	 *             $result = ....
646
	 *         }
647
	 *         return $result;
648
	 *     }
649
	 * @endcode
650
	 *
651
	 * @param callable $workCallback
652
	 * @since 1.28
653
	 */
654
	public function addBusyCallback( callable $workCallback ) {
655
		$this->busyCallbacks[] = $workCallback;
656
	}
657
658
	/**
659
	 * Modify a cache update operation array for EventRelayer::notify()
660
	 *
661
	 * This is used for relayed writes, e.g. for broadcasting a change
662
	 * to multiple data-centers. If the array contains a 'val' field
663
	 * then the command involves setting a key to that value. Note that
664
	 * for simplicity, 'val' is always a simple scalar value. This method
665
	 * is used to possibly serialize the value and add any cache-specific
666
	 * key/values needed for the relayer daemon (e.g. memcached flags).
667
	 *
668
	 * @param array $event
669
	 * @return array
670
	 * @since 1.26
671
	 */
672
	public function modifySimpleRelayEvent( array $event ) {
673
		return $event;
674
	}
675
676
	/**
677
	 * @param string $text
678
	 */
679
	protected function debug( $text ) {
680
		if ( $this->debugMode ) {
681
			$this->logger->debug( "{class} debug: $text", [
682
				'class' => get_class( $this ),
683
			] );
684
		}
685
	}
686
687
	/**
688
	 * Convert an optionally relative time to an absolute time
689
	 * @param int $exptime
690
	 * @return int
691
	 */
692
	protected function convertExpiry( $exptime ) {
693
		if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
694
			return time() + $exptime;
695
		} else {
696
			return $exptime;
697
		}
698
	}
699
700
	/**
701
	 * Convert an optionally absolute expiry time to a relative time. If an
702
	 * absolute time is specified which is in the past, use a short expiry time.
703
	 *
704
	 * @param int $exptime
705
	 * @return int
706
	 */
707
	protected function convertToRelative( $exptime ) {
708
		if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
709
			$exptime -= time();
710
			if ( $exptime <= 0 ) {
711
				$exptime = 1;
712
			}
713
			return $exptime;
714
		} else {
715
			return $exptime;
716
		}
717
	}
718
719
	/**
720
	 * Check if a value is an integer
721
	 *
722
	 * @param mixed $value
723
	 * @return bool
724
	 */
725
	protected function isInteger( $value ) {
726
		return ( is_int( $value ) || ctype_digit( $value ) );
727
	}
728
729
	/**
730
	 * Construct a cache key.
731
	 *
732
	 * @since 1.27
733
	 * @param string $keyspace
734
	 * @param array $args
735
	 * @return string
736
	 */
737
	public function makeKeyInternal( $keyspace, $args ) {
738
		$key = $keyspace;
739
		foreach ( $args as $arg ) {
740
			$arg = str_replace( ':', '%3A', $arg );
741
			$key = $key . ':' . $arg;
742
		}
743
		return strtr( $key, ' ', '_' );
744
	}
745
746
	/**
747
	 * Make a global cache key.
748
	 *
749
	 * @since 1.27
750
	 * @param string ... Key component (variadic)
751
	 * @return string
752
	 */
753
	public function makeGlobalKey() {
754
		return $this->makeKeyInternal( 'global', func_get_args() );
755
	}
756
757
	/**
758
	 * Make a cache key, scoped to this instance's keyspace.
759
	 *
760
	 * @since 1.27
761
	 * @param string ... Key component (variadic)
762
	 * @return string
763
	 */
764
	public function makeKey() {
765
		return $this->makeKeyInternal( $this->keyspace, func_get_args() );
766
	}
767
768
	/**
769
	 * @param integer $flag ATTR_* class constant
770
	 * @return integer QOS_* class constant
771
	 * @since 1.28
772
	 */
773
	public function getQoS( $flag ) {
774
		return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] : self::QOS_UNKNOWN;
775
	}
776
777
	/**
778
	 * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
779
	 *
780
	 * @param BagOStuff[] $bags
781
	 * @return integer[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
782
	 */
783
	protected function mergeFlagMaps( array $bags ) {
784
		$map = [];
785
		foreach ( $bags as $bag ) {
786
			foreach ( $bag->attrMap as $attr => $rank ) {
787 View Code Duplication
				if ( isset( $map[$attr] ) ) {
788
					$map[$attr] = min( $map[$attr], $rank );
789
				} else {
790
					$map[$attr] = $rank;
791
				}
792
			}
793
		}
794
795
		return $map;
796
	}
797
}
798