Completed
Branch master (467086)
by
unknown
30:56
created

BagOStuff::debug()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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