Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like WANObjectCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use WANObjectCache, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 67 | class WANObjectCache implements IExpiringStore, LoggerAwareInterface { |
||
| 68 | /** @var BagOStuff The local datacenter cache */ |
||
| 69 | protected $cache; |
||
| 70 | /** @var HashBagOStuff Script instance PHP cache */ |
||
| 71 | protected $procCache; |
||
| 72 | /** @var string Purge channel name */ |
||
| 73 | protected $purgeChannel; |
||
| 74 | /** @var EventRelayer Bus that handles purge broadcasts */ |
||
| 75 | protected $purgeRelayer; |
||
| 76 | /** @var LoggerInterface */ |
||
| 77 | protected $logger; |
||
| 78 | |||
| 79 | /** @var int ERR_* constant for the "last error" registry */ |
||
| 80 | protected $lastRelayError = self::ERR_NONE; |
||
| 81 | |||
| 82 | /** Max time expected to pass between delete() and DB commit finishing */ |
||
| 83 | const MAX_COMMIT_DELAY = 3; |
||
| 84 | /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */ |
||
| 85 | const MAX_READ_LAG = 7; |
||
| 86 | /** Seconds to tombstone keys on delete() */ |
||
| 87 | const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1 |
||
| 88 | |||
| 89 | /** Seconds to keep dependency purge keys around */ |
||
| 90 | const CHECK_KEY_TTL = self::TTL_YEAR; |
||
| 91 | /** Seconds to keep lock keys around */ |
||
| 92 | const LOCK_TTL = 10; |
||
| 93 | /** Default remaining TTL at which to consider pre-emptive regeneration */ |
||
| 94 | const LOW_TTL = 30; |
||
| 95 | /** Default time-since-expiry on a miss that makes a key "hot" */ |
||
| 96 | const LOCK_TSE = 1; |
||
| 97 | |||
| 98 | /** Idiom for getWithSetCallback() callbacks to avoid calling set() */ |
||
| 99 | const TTL_UNCACHEABLE = -1; |
||
| 100 | /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */ |
||
| 101 | const TSE_NONE = -1; |
||
| 102 | /** Max TTL to store keys when a data sourced is lagged */ |
||
| 103 | const TTL_LAGGED = 30; |
||
| 104 | /** Idiom for delete() for "no hold-off" */ |
||
| 105 | const HOLDOFF_NONE = 0; |
||
| 106 | |||
| 107 | /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */ |
||
| 108 | const TINY_NEGATIVE = -0.000001; |
||
| 109 | |||
| 110 | /** Cache format version number */ |
||
| 111 | const VERSION = 1; |
||
| 112 | |||
| 113 | const FLD_VERSION = 0; // key to cache version number |
||
| 114 | const FLD_VALUE = 1; // key to the cached value |
||
| 115 | const FLD_TTL = 2; // key to the original TTL |
||
| 116 | const FLD_TIME = 3; // key to the cache time |
||
| 117 | const FLD_FLAGS = 4; // key to the flags bitfield |
||
| 118 | const FLD_HOLDOFF = 5; // key to any hold-off TTL |
||
| 119 | |||
| 120 | /** @var integer Treat this value as expired-on-arrival */ |
||
| 121 | const FLG_STALE = 1; |
||
| 122 | |||
| 123 | const ERR_NONE = 0; // no error |
||
| 124 | const ERR_NO_RESPONSE = 1; // no response |
||
| 125 | const ERR_UNREACHABLE = 2; // can't connect |
||
| 126 | const ERR_UNEXPECTED = 3; // response gave some error |
||
| 127 | const ERR_RELAY = 4; // relay broadcast failed |
||
| 128 | |||
| 129 | const VALUE_KEY_PREFIX = 'WANCache:v:'; |
||
| 130 | const INTERIM_KEY_PREFIX = 'WANCache:i:'; |
||
| 131 | const TIME_KEY_PREFIX = 'WANCache:t:'; |
||
| 132 | |||
| 133 | const PURGE_VAL_PREFIX = 'PURGED:'; |
||
| 134 | |||
| 135 | const VFLD_DATA = 'WOC:d'; // key to the value of versioned data |
||
| 136 | const VFLD_VERSION = 'WOC:v'; // key to the version of the value present |
||
| 137 | |||
| 138 | const MAX_PC_KEYS = 1000; // max keys to keep in process cache |
||
| 139 | |||
| 140 | const DEFAULT_PURGE_CHANNEL = 'wancache-purge'; |
||
| 141 | |||
| 142 | /** |
||
| 143 | * @param array $params |
||
| 144 | * - cache : BagOStuff object for a persistent cache |
||
| 145 | * - channels : Map of (action => channel string). Actions include "purge". |
||
| 146 | * - relayers : Map of (action => EventRelayer object). Actions include "purge". |
||
| 147 | * - logger : LoggerInterface object |
||
| 148 | */ |
||
| 149 | public function __construct( array $params ) { |
||
| 160 | |||
| 161 | public function setLogger( LoggerInterface $logger ) { |
||
| 164 | |||
| 165 | /** |
||
| 166 | * Get an instance that wraps EmptyBagOStuff |
||
| 167 | * |
||
| 168 | * @return WANObjectCache |
||
| 169 | */ |
||
| 170 | public static function newEmpty() { |
||
| 177 | |||
| 178 | /** |
||
| 179 | * Fetch the value of a key from cache |
||
| 180 | * |
||
| 181 | * If supplied, $curTTL is set to the remaining TTL (current time left): |
||
| 182 | * - a) INF; if $key exists, has no TTL, and is not expired by $checkKeys |
||
| 183 | * - b) float (>=0); if $key exists, has a TTL, and is not expired by $checkKeys |
||
| 184 | * - c) float (<0); if $key is tombstoned, stale, or existing but expired by $checkKeys |
||
| 185 | * - d) null; if $key does not exist and is not tombstoned |
||
| 186 | * |
||
| 187 | * If a key is tombstoned, $curTTL will reflect the time since delete(). |
||
| 188 | * |
||
| 189 | * The timestamp of $key will be checked against the last-purge timestamp |
||
| 190 | * of each of $checkKeys. Those $checkKeys not in cache will have the last-purge |
||
| 191 | * initialized to the current timestamp. If any of $checkKeys have a timestamp |
||
| 192 | * greater than that of $key, then $curTTL will reflect how long ago $key |
||
| 193 | * became invalid. Callers can use $curTTL to know when the value is stale. |
||
| 194 | * The $checkKeys parameter allow mass invalidations by updating a single key: |
||
| 195 | * - a) Each "check" key represents "last purged" of some source data |
||
| 196 | * - b) Callers pass in relevant "check" keys as $checkKeys in get() |
||
| 197 | * - c) When the source data that "check" keys represent changes, |
||
| 198 | * the touchCheckKey() method is called on them |
||
| 199 | * |
||
| 200 | * Source data entities might exists in a DB that uses snapshot isolation |
||
| 201 | * (e.g. the default REPEATABLE-READ in innoDB). Even for mutable data, that |
||
| 202 | * isolation can largely be maintained by doing the following: |
||
| 203 | * - a) Calling delete() on entity change *and* creation, before DB commit |
||
| 204 | * - b) Keeping transaction duration shorter than delete() hold-off TTL |
||
| 205 | * |
||
| 206 | * However, pre-snapshot values might still be seen if an update was made |
||
| 207 | * in a remote datacenter but the purge from delete() didn't relay yet. |
||
| 208 | * |
||
| 209 | * Consider using getWithSetCallback() instead of get() and set() cycles. |
||
| 210 | * That method has cache slam avoiding features for hot/expensive keys. |
||
| 211 | * |
||
| 212 | * @param string $key Cache key |
||
| 213 | * @param mixed $curTTL Approximate TTL left on the key if present/tombstoned [returned] |
||
| 214 | * @param array $checkKeys List of "check" keys |
||
| 215 | * @param float &$asOf UNIX timestamp of cached value; null on failure [returned] |
||
| 216 | * @return mixed Value of cache key or false on failure |
||
| 217 | */ |
||
| 218 | final public function get( $key, &$curTTL = null, array $checkKeys = [], &$asOf = null ) { |
||
| 219 | $curTTLs = []; |
||
| 220 | $asOfs = []; |
||
| 221 | $values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $asOfs ); |
||
| 222 | $curTTL = isset( $curTTLs[$key] ) ? $curTTLs[$key] : null; |
||
| 223 | $asOf = isset( $asOfs[$key] ) ? $asOfs[$key] : null; |
||
| 224 | |||
| 225 | return isset( $values[$key] ) ? $values[$key] : false; |
||
| 226 | } |
||
| 227 | |||
| 228 | /** |
||
| 229 | * Fetch the value of several keys from cache |
||
| 230 | * |
||
| 231 | * @see WANObjectCache::get() |
||
| 232 | * |
||
| 233 | * @param array $keys List of cache keys |
||
| 234 | * @param array $curTTLs Map of (key => approximate TTL left) for existing keys [returned] |
||
| 235 | * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check" |
||
| 236 | * keys to specific cache keys only by using cache keys as keys in the $checkKeys array. |
||
| 237 | * @param float[] &$asOfs Map of (key => UNIX timestamp of cached value; null on failure) |
||
| 238 | * @return array Map of (key => value) for keys that exist |
||
| 239 | */ |
||
| 240 | final public function getMulti( |
||
| 241 | array $keys, &$curTTLs = [], array $checkKeys = [], array &$asOfs = [] |
||
| 242 | ) { |
||
| 243 | $result = []; |
||
| 244 | $curTTLs = []; |
||
| 245 | $asOfs = []; |
||
| 246 | |||
| 247 | $vPrefixLen = strlen( self::VALUE_KEY_PREFIX ); |
||
| 248 | $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX ); |
||
| 249 | |||
| 250 | $checkKeysForAll = []; |
||
| 251 | $checkKeysByKey = []; |
||
| 252 | $checkKeysFlat = []; |
||
| 253 | foreach ( $checkKeys as $i => $keys ) { |
||
| 254 | $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX ); |
||
| 255 | $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed ); |
||
| 256 | // Is this check keys for a specific cache key, or for all keys being fetched? |
||
| 257 | if ( is_int( $i ) ) { |
||
| 258 | $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed ); |
||
| 259 | } else { |
||
| 260 | $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] ) |
||
| 261 | ? array_merge( $checkKeysByKey[$i], $prefixed ) |
||
| 262 | : $prefixed; |
||
| 263 | } |
||
| 264 | } |
||
| 265 | |||
| 266 | // Fetch all of the raw values |
||
| 267 | $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) ); |
||
| 268 | // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic) |
||
| 269 | $now = microtime( true ); |
||
| 270 | |||
| 271 | // Collect timestamps from all "check" keys |
||
| 272 | $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now ); |
||
| 273 | $purgeValuesByKey = []; |
||
| 274 | foreach ( $checkKeysByKey as $cacheKey => $checks ) { |
||
| 275 | $purgeValuesByKey[$cacheKey] = |
||
| 276 | $this->processCheckKeys( $checks, $wrappedValues, $now ); |
||
| 277 | } |
||
| 278 | |||
| 279 | // Get the main cache value for each key and validate them |
||
| 280 | foreach ( $valueKeys as $vKey ) { |
||
| 281 | if ( !isset( $wrappedValues[$vKey] ) ) { |
||
| 282 | continue; // not found |
||
| 283 | } |
||
| 284 | |||
| 285 | $key = substr( $vKey, $vPrefixLen ); // unprefix |
||
| 286 | |||
| 287 | list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now ); |
||
| 288 | if ( $value !== false ) { |
||
| 289 | $result[$key] = $value; |
||
| 290 | |||
| 291 | // Force dependant keys to be invalid for a while after purging |
||
| 292 | // to reduce race conditions involving stale data getting cached |
||
| 293 | $purgeValues = $purgeValuesForAll; |
||
| 294 | if ( isset( $purgeValuesByKey[$key] ) ) { |
||
| 295 | $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] ); |
||
| 296 | } |
||
| 297 | foreach ( $purgeValues as $purge ) { |
||
| 298 | $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF]; |
||
| 299 | if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) { |
||
| 300 | // How long ago this value was expired by *this* check key |
||
| 301 | $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE ); |
||
| 302 | // How long ago this value was expired by *any* known check key |
||
| 303 | $curTTL = min( $curTTL, $ago ); |
||
| 304 | } |
||
| 305 | } |
||
| 306 | } |
||
| 307 | $curTTLs[$key] = $curTTL; |
||
| 308 | $asOfs[$key] = ( $value !== false ) ? $wrappedValues[$vKey][self::FLD_TIME] : null; |
||
| 309 | } |
||
| 310 | |||
| 311 | return $result; |
||
| 312 | } |
||
| 313 | |||
| 314 | /** |
||
| 315 | * @since 1.27 |
||
| 316 | * @param array $timeKeys List of prefixed time check keys |
||
| 317 | * @param array $wrappedValues |
||
| 318 | * @param float $now |
||
| 319 | * @return array List of purge value arrays |
||
| 320 | */ |
||
| 321 | private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) { |
||
| 337 | |||
| 338 | /** |
||
| 339 | * Set the value of a key in cache |
||
| 340 | * |
||
| 341 | * Simply calling this method when source data changes is not valid because |
||
| 342 | * the changes do not replicate to the other WAN sites. In that case, delete() |
||
| 343 | * should be used instead. This method is intended for use on cache misses. |
||
| 344 | * |
||
| 345 | * If the data was read from a snapshot-isolated transactions (e.g. the default |
||
| 346 | * REPEATABLE-READ in innoDB), use 'since' to avoid the following race condition: |
||
| 347 | * - a) T1 starts |
||
| 348 | * - b) T2 updates a row, calls delete(), and commits |
||
| 349 | * - c) The HOLDOFF_TTL passes, expiring the delete() tombstone |
||
| 350 | * - d) T1 reads the row and calls set() due to a cache miss |
||
| 351 | * - e) Stale value is stuck in cache |
||
| 352 | * |
||
| 353 | * Setting 'lag' and 'since' help avoids keys getting stuck in stale states. |
||
| 354 | * |
||
| 355 | * Example usage: |
||
| 356 | * @code |
||
| 357 | * $dbr = wfGetDB( DB_SLAVE ); |
||
| 358 | * $setOpts = Database::getCacheSetOptions( $dbr ); |
||
| 359 | * // Fetch the row from the DB |
||
| 360 | * $row = $dbr->selectRow( ... ); |
||
| 361 | * $key = $cache->makeKey( 'building', $buildingId ); |
||
| 362 | * $cache->set( $key, $row, $cache::TTL_DAY, $setOpts ); |
||
| 363 | * @endcode |
||
| 364 | * |
||
| 365 | * @param string $key Cache key |
||
| 366 | * @param mixed $value |
||
| 367 | * @param integer $ttl Seconds to live. Special values are: |
||
| 368 | * - WANObjectCache::TTL_INDEFINITE: Cache forever |
||
| 369 | * @param array $opts Options map: |
||
| 370 | * - lag : Seconds of slave lag. Typically, this is either the slave lag |
||
| 371 | * before the data was read or, if applicable, the slave lag before |
||
| 372 | * the snapshot-isolated transaction the data was read from started. |
||
| 373 | * Default: 0 seconds |
||
| 374 | * - since : UNIX timestamp of the data in $value. Typically, this is either |
||
| 375 | * the current time the data was read or (if applicable) the time when |
||
| 376 | * the snapshot-isolated transaction the data was read from started. |
||
| 377 | * Default: 0 seconds |
||
| 378 | * - pending : Whether this data is possibly from an uncommitted write transaction. |
||
| 379 | * Generally, other threads should not see values from the future and |
||
| 380 | * they certainly should not see ones that ended up getting rolled back. |
||
| 381 | * Default: false |
||
| 382 | * - lockTSE : if excessive replication/snapshot lag is detected, then store the value |
||
| 383 | * with this TTL and flag it as stale. This is only useful if the reads for |
||
| 384 | * this key use getWithSetCallback() with "lockTSE" set. |
||
| 385 | * Default: WANObjectCache::TSE_NONE |
||
| 386 | * @return bool Success |
||
| 387 | */ |
||
| 388 | final public function set( $key, $value, $ttl = 0, array $opts = [] ) { |
||
| 436 | |||
| 437 | /** |
||
| 438 | * Purge a key from all datacenters |
||
| 439 | * |
||
| 440 | * This should only be called when the underlying data (being cached) |
||
| 441 | * changes in a significant way. This deletes the key and starts a hold-off |
||
| 442 | * period where the key cannot be written to for a few seconds (HOLDOFF_TTL). |
||
| 443 | * This is done to avoid the following race condition: |
||
| 444 | * - a) Some DB data changes and delete() is called on a corresponding key |
||
| 445 | * - b) A request refills the key with a stale value from a lagged DB |
||
| 446 | * - c) The stale value is stuck there until the key is expired/evicted |
||
| 447 | * |
||
| 448 | * This is implemented by storing a special "tombstone" value at the cache |
||
| 449 | * key that this class recognizes; get() calls will return false for the key |
||
| 450 | * and any set() calls will refuse to replace tombstone values at the key. |
||
| 451 | * For this to always avoid stale value writes, the following must hold: |
||
| 452 | * - a) Replication lag is bounded to being less than HOLDOFF_TTL; or |
||
| 453 | * - b) If lag is higher, the DB will have gone into read-only mode already |
||
| 454 | * |
||
| 455 | * Note that set() can also be lag-aware and lower the TTL if it's high. |
||
| 456 | * |
||
| 457 | * When using potentially long-running ACID transactions, a good pattern is |
||
| 458 | * to use a pre-commit hook to issue the delete. This means that immediately |
||
| 459 | * after commit, callers will see the tombstone in cache in the local datacenter |
||
| 460 | * and in the others upon relay. It also avoids the following race condition: |
||
| 461 | * - a) T1 begins, changes a row, and calls delete() |
||
| 462 | * - b) The HOLDOFF_TTL passes, expiring the delete() tombstone |
||
| 463 | * - c) T2 starts, reads the row and calls set() due to a cache miss |
||
| 464 | * - d) T1 finally commits |
||
| 465 | * - e) Stale value is stuck in cache |
||
| 466 | * |
||
| 467 | * Example usage: |
||
| 468 | * @code |
||
| 469 | * $dbw->begin( __METHOD__ ); // start of request |
||
| 470 | * ... <execute some stuff> ... |
||
| 471 | * // Update the row in the DB |
||
| 472 | * $dbw->update( ... ); |
||
| 473 | * $key = $cache->makeKey( 'homes', $homeId ); |
||
| 474 | * // Purge the corresponding cache entry just before committing |
||
| 475 | * $dbw->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) { |
||
| 476 | * $cache->delete( $key ); |
||
| 477 | * } ); |
||
| 478 | * ... <execute some stuff> ... |
||
| 479 | * $dbw->commit( __METHOD__ ); // end of request |
||
| 480 | * @endcode |
||
| 481 | * |
||
| 482 | * The $ttl parameter can be used when purging values that have not actually changed |
||
| 483 | * recently. For example, a cleanup script to purge cache entries does not really need |
||
| 484 | * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge. |
||
| 485 | * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback(). |
||
| 486 | * |
||
| 487 | * If called twice on the same key, then the last hold-off TTL takes precedence. For |
||
| 488 | * idempotence, the $ttl should not vary for different delete() calls on the same key. |
||
| 489 | * |
||
| 490 | * @param string $key Cache key |
||
| 491 | * @param integer $ttl Tombstone TTL; Default: WANObjectCache::HOLDOFF_TTL |
||
| 492 | * @return bool True if the item was purged or not found, false on failure |
||
| 493 | */ |
||
| 494 | final public function delete( $key, $ttl = self::HOLDOFF_TTL ) { |
||
| 514 | |||
| 515 | /** |
||
| 516 | * Fetch the value of a timestamp "check" key |
||
| 517 | * |
||
| 518 | * The key will be *initialized* to the current time if not set, |
||
| 519 | * so only call this method if this behavior is actually desired |
||
| 520 | * |
||
| 521 | * The timestamp can be used to check whether a cached value is valid. |
||
| 522 | * Callers should not assume that this returns the same timestamp in |
||
| 523 | * all datacenters due to relay delays. |
||
| 524 | * |
||
| 525 | * The level of staleness can roughly be estimated from this key, but |
||
| 526 | * if the key was evicted from cache, such calculations may show the |
||
| 527 | * time since expiry as ~0 seconds. |
||
| 528 | * |
||
| 529 | * Note that "check" keys won't collide with other regular keys. |
||
| 530 | * |
||
| 531 | * @param string $key |
||
| 532 | * @return float UNIX timestamp of the check key |
||
| 533 | */ |
||
| 534 | final public function getCheckKeyTime( $key ) { |
||
| 552 | |||
| 553 | /** |
||
| 554 | * Purge a "check" key from all datacenters, invalidating keys that use it |
||
| 555 | * |
||
| 556 | * This should only be called when the underlying data (being cached) |
||
| 557 | * changes in a significant way, and it is impractical to call delete() |
||
| 558 | * on all keys that should be changed. When get() is called on those |
||
| 559 | * keys, the relevant "check" keys must be supplied for this to work. |
||
| 560 | * |
||
| 561 | * The "check" key essentially represents a last-modified field. |
||
| 562 | * When touched, keys using it via get(), getMulti(), or getWithSetCallback() |
||
| 563 | * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future |
||
| 564 | * by those methods to avoid race conditions where dependent keys get updated |
||
| 565 | * with stale values (e.g. from a DB slave). |
||
| 566 | * |
||
| 567 | * This is typically useful for keys with hardcoded names or in some cases |
||
| 568 | * dynamically generated names where a low number of combinations exist. |
||
| 569 | * When a few important keys get a large number of hits, a high cache |
||
| 570 | * time is usually desired as well as "lockTSE" logic. The resetCheckKey() |
||
| 571 | * method is less appropriate in such cases since the "time since expiry" |
||
| 572 | * cannot be inferred. |
||
| 573 | * |
||
| 574 | * Note that "check" keys won't collide with other regular keys. |
||
| 575 | * |
||
| 576 | * @see WANObjectCache::get() |
||
| 577 | * @see WANObjectCache::getWithSetCallback() |
||
| 578 | * @see WANObjectCache::resetCheckKey() |
||
| 579 | * |
||
| 580 | * @param string $key Cache key |
||
| 581 | * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant |
||
| 582 | * @return bool True if the item was purged or not found, false on failure |
||
| 583 | */ |
||
| 584 | final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) { |
||
| 594 | |||
| 595 | /** |
||
| 596 | * Delete a "check" key from all datacenters, invalidating keys that use it |
||
| 597 | * |
||
| 598 | * This is similar to touchCheckKey() in that keys using it via get(), getMulti(), |
||
| 599 | * or getWithSetCallback() will be invalidated. The differences are: |
||
| 600 | * - a) The timestamp will be deleted from all caches and lazily |
||
| 601 | * re-initialized when accessed (rather than set everywhere) |
||
| 602 | * - b) Thus, dependent keys will be known to be invalid, but not |
||
| 603 | * for how long (they are treated as "just" purged), which |
||
| 604 | * effects any lockTSE logic in getWithSetCallback() |
||
| 605 | * |
||
| 606 | * The advantage is that this does not place high TTL keys on every cache |
||
| 607 | * server, making it better for code that will cache many different keys |
||
| 608 | * and either does not use lockTSE or uses a low enough TTL anyway. |
||
| 609 | * |
||
| 610 | * This is typically useful for keys with dynamically generated names |
||
| 611 | * where a high number of combinations exist. |
||
| 612 | * |
||
| 613 | * Note that "check" keys won't collide with other regular keys. |
||
| 614 | * |
||
| 615 | * @see WANObjectCache::get() |
||
| 616 | * @see WANObjectCache::getWithSetCallback() |
||
| 617 | * @see WANObjectCache::touchCheckKey() |
||
| 618 | * |
||
| 619 | * @param string $key Cache key |
||
| 620 | * @return bool True if the item was purged or not found, false on failure |
||
| 621 | */ |
||
| 622 | final public function resetCheckKey( $key ) { |
||
| 629 | |||
| 630 | /** |
||
| 631 | * Method to fetch/regenerate cache keys |
||
| 632 | * |
||
| 633 | * On cache miss, the key will be set to the callback result via set() |
||
| 634 | * (unless the callback returns false) and that result will be returned. |
||
| 635 | * The arguments supplied to the callback are: |
||
| 636 | * - $oldValue : current cache value or false if not present |
||
| 637 | * - &$ttl : a reference to the TTL which can be altered |
||
| 638 | * - &$setOpts : a reference to options for set() which can be altered |
||
| 639 | * |
||
| 640 | * It is strongly recommended to set the 'lag' and 'since' fields to avoid race conditions |
||
| 641 | * that can cause stale values to get stuck at keys. Usually, callbacks ignore the current |
||
| 642 | * value, but it can be used to maintain "most recent X" values that come from time or |
||
| 643 | * sequence based source data, provided that the "as of" id/time is tracked. Note that |
||
| 644 | * preemptive regeneration and $checkKeys can result in a non-false current value. |
||
| 645 | * |
||
| 646 | * Usage of $checkKeys is similar to get() and getMulti(). However, rather than the caller |
||
| 647 | * having to inspect a "current time left" variable (e.g. $curTTL, $curTTLs), a cache |
||
| 648 | * regeneration will automatically be triggered using the callback. |
||
| 649 | * |
||
| 650 | * The simplest way to avoid stampedes for hot keys is to use |
||
| 651 | * the 'lockTSE' option in $opts. If cache purges are needed, also: |
||
| 652 | * - a) Pass $key into $checkKeys |
||
| 653 | * - b) Use touchCheckKey( $key ) instead of delete( $key ) |
||
| 654 | * |
||
| 655 | * Example usage (typical key): |
||
| 656 | * @code |
||
| 657 | * $catInfo = $cache->getWithSetCallback( |
||
| 658 | * // Key to store the cached value under |
||
| 659 | * $cache->makeKey( 'cat-attributes', $catId ), |
||
| 660 | * // Time-to-live (in seconds) |
||
| 661 | * $cache::TTL_MINUTE, |
||
| 662 | * // Function that derives the new key value |
||
| 663 | * function ( $oldValue, &$ttl, array &$setOpts ) { |
||
| 664 | * $dbr = wfGetDB( DB_SLAVE ); |
||
| 665 | * // Account for any snapshot/slave lag |
||
| 666 | * $setOpts += Database::getCacheSetOptions( $dbr ); |
||
| 667 | * |
||
| 668 | * return $dbr->selectRow( ... ); |
||
| 669 | * } |
||
| 670 | * ); |
||
| 671 | * @endcode |
||
| 672 | * |
||
| 673 | * Example usage (key that is expensive and hot): |
||
| 674 | * @code |
||
| 675 | * $catConfig = $cache->getWithSetCallback( |
||
| 676 | * // Key to store the cached value under |
||
| 677 | * $cache->makeKey( 'site-cat-config' ), |
||
| 678 | * // Time-to-live (in seconds) |
||
| 679 | * $cache::TTL_DAY, |
||
| 680 | * // Function that derives the new key value |
||
| 681 | * function ( $oldValue, &$ttl, array &$setOpts ) { |
||
| 682 | * $dbr = wfGetDB( DB_SLAVE ); |
||
| 683 | * // Account for any snapshot/slave lag |
||
| 684 | * $setOpts += Database::getCacheSetOptions( $dbr ); |
||
| 685 | * |
||
| 686 | * return CatConfig::newFromRow( $dbr->selectRow( ... ) ); |
||
| 687 | * }, |
||
| 688 | * [ |
||
| 689 | * // Calling touchCheckKey() on this key invalidates the cache |
||
| 690 | * 'checkKeys' => [ $cache->makeKey( 'site-cat-config' ) ], |
||
| 691 | * // Try to only let one datacenter thread manage cache updates at a time |
||
| 692 | * 'lockTSE' => 30 |
||
| 693 | * ] |
||
| 694 | * ); |
||
| 695 | * @endcode |
||
| 696 | * |
||
| 697 | * Example usage (key with dynamic dependencies): |
||
| 698 | * @code |
||
| 699 | * $catState = $cache->getWithSetCallback( |
||
| 700 | * // Key to store the cached value under |
||
| 701 | * $cache->makeKey( 'cat-state', $cat->getId() ), |
||
| 702 | * // Time-to-live (seconds) |
||
| 703 | * $cache::TTL_HOUR, |
||
| 704 | * // Function that derives the new key value |
||
| 705 | * function ( $oldValue, &$ttl, array &$setOpts ) { |
||
| 706 | * // Determine new value from the DB |
||
| 707 | * $dbr = wfGetDB( DB_SLAVE ); |
||
| 708 | * // Account for any snapshot/slave lag |
||
| 709 | * $setOpts += Database::getCacheSetOptions( $dbr ); |
||
| 710 | * |
||
| 711 | * return CatState::newFromResults( $dbr->select( ... ) ); |
||
| 712 | * }, |
||
| 713 | * [ |
||
| 714 | * // The "check" keys that represent things the value depends on; |
||
| 715 | * // Calling touchCheckKey() on any of them invalidates the cache |
||
| 716 | * 'checkKeys' => [ |
||
| 717 | * $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ), |
||
| 718 | * $cache->makeKey( 'people-present', $cat->getHouseId() ), |
||
| 719 | * $cache->makeKey( 'cat-laws', $cat->getCityId() ), |
||
| 720 | * ] |
||
| 721 | * ] |
||
| 722 | * ); |
||
| 723 | * @endcode |
||
| 724 | * |
||
| 725 | * Example usage (hot key holding most recent 100 events): |
||
| 726 | * @code |
||
| 727 | * $lastCatActions = $cache->getWithSetCallback( |
||
| 728 | * // Key to store the cached value under |
||
| 729 | * $cache->makeKey( 'cat-last-actions', 100 ), |
||
| 730 | * // Time-to-live (in seconds) |
||
| 731 | * 10, |
||
| 732 | * // Function that derives the new key value |
||
| 733 | * function ( $oldValue, &$ttl, array &$setOpts ) { |
||
| 734 | * $dbr = wfGetDB( DB_SLAVE ); |
||
| 735 | * // Account for any snapshot/slave lag |
||
| 736 | * $setOpts += Database::getCacheSetOptions( $dbr ); |
||
| 737 | * |
||
| 738 | * // Start off with the last cached list |
||
| 739 | * $list = $oldValue ?: []; |
||
| 740 | * // Fetch the last 100 relevant rows in descending order; |
||
| 741 | * // only fetch rows newer than $list[0] to reduce scanning |
||
| 742 | * $rows = iterator_to_array( $dbr->select( ... ) ); |
||
| 743 | * // Merge them and get the new "last 100" rows |
||
| 744 | * return array_slice( array_merge( $new, $list ), 0, 100 ); |
||
| 745 | * }, |
||
| 746 | * // Try to only let one datacenter thread manage cache updates at a time |
||
| 747 | * [ 'lockTSE' => 30 ] |
||
| 748 | * ); |
||
| 749 | * @endcode |
||
| 750 | * |
||
| 751 | * @see WANObjectCache::get() |
||
| 752 | * @see WANObjectCache::set() |
||
| 753 | * |
||
| 754 | * @param string $key Cache key |
||
| 755 | * @param integer $ttl Seconds to live for key updates. Special values are: |
||
| 756 | * - WANObjectCache::TTL_INDEFINITE: Cache forever |
||
| 757 | * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all |
||
| 758 | * @param callable $callback Value generation function |
||
| 759 | * @param array $opts Options map: |
||
| 760 | * - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either |
||
| 761 | * touchCheckKey() or resetCheckKey() is called on any of these keys. |
||
| 762 | * Default: []. |
||
| 763 | * - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less |
||
| 764 | * than this. It becomes more likely over time, becoming certain once the key is expired. |
||
| 765 | * Default: WANObjectCache::LOW_TTL. |
||
| 766 | * - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds |
||
| 767 | * ago, then try to have a single thread handle cache regeneration at any given time. |
||
| 768 | * Other threads will try to use stale values if possible. If, on miss, the time since |
||
| 769 | * expiration is low, the assumption is that the key is hot and that a stampede is worth |
||
| 770 | * avoiding. Setting this above WANObjectCache::HOLDOFF_TTL makes no difference. The |
||
| 771 | * higher this is set, the higher the worst-case staleness can be. |
||
| 772 | * Use WANObjectCache::TSE_NONE to disable this logic. |
||
| 773 | * Default: WANObjectCache::TSE_NONE. |
||
| 774 | * - busyValue: If no value exists and another thread is currently regenerating it, use this |
||
| 775 | * as a fallback value (or a callback to generate such a value). This assures that cache |
||
| 776 | * stampedes cannot happen if the value falls out of cache. This can be used as insurance |
||
| 777 | * against cache regeneration becoming very slow for some reason (greater than the TTL). |
||
| 778 | * Default: null. |
||
| 779 | * - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids |
||
| 780 | * network I/O when a key is read several times. This will not cache when the callback |
||
| 781 | * returns false, however. Note that any purges will not be seen while process cached; |
||
| 782 | * since the callback should use slave DBs and they may be lagged or have snapshot |
||
| 783 | * isolation anyway, this should not typically matter. |
||
| 784 | * Default: WANObjectCache::TTL_UNCACHEABLE. |
||
| 785 | * - version: Integer version number. This allows for callers to make breaking changes to |
||
| 786 | * how values are stored while maintaining compatability and correct cache purges. New |
||
| 787 | * versions are stored alongside older versions concurrently. Avoid storing class objects |
||
| 788 | * however, as this reduces compatibility (due to serialization). |
||
| 789 | * Default: null. |
||
| 790 | * @return mixed Value found or written to the key |
||
| 791 | * @note Callable type hints are not used to avoid class-autoloading |
||
| 792 | */ |
||
| 793 | final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) { |
||
| 853 | |||
| 854 | /** |
||
| 855 | * Do the actual I/O for getWithSetCallback() when needed |
||
| 856 | * |
||
| 857 | * @see WANObjectCache::getWithSetCallback() |
||
| 858 | * |
||
| 859 | * @param string $key |
||
| 860 | * @param integer $ttl |
||
| 861 | * @param callback $callback |
||
| 862 | * @param array $opts Options map for getWithSetCallback() which also includes: |
||
| 863 | * - minTime: Treat values older than this UNIX timestamp as not existing. Default: null. |
||
| 864 | * @param float &$asOf Cache generation timestamp of returned value [returned] |
||
| 865 | * @return mixed |
||
| 866 | * @note Callable type hints are not used to avoid class-autoloading |
||
| 867 | */ |
||
| 868 | protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) { |
||
| 957 | |||
| 958 | /** |
||
| 959 | * @see BagOStuff::makeKey() |
||
| 960 | * @param string ... Key component |
||
| 961 | * @return string |
||
| 962 | * @since 1.27 |
||
| 963 | */ |
||
| 964 | public function makeKey() { |
||
| 967 | |||
| 968 | /** |
||
| 969 | * @see BagOStuff::makeGlobalKey() |
||
| 970 | * @param string ... Key component |
||
| 971 | * @return string |
||
| 972 | * @since 1.27 |
||
| 973 | */ |
||
| 974 | public function makeGlobalKey() { |
||
| 977 | |||
| 978 | /** |
||
| 979 | * Get the "last error" registered; clearLastError() should be called manually |
||
| 980 | * @return int ERR_* class constant for the "last error" registry |
||
| 981 | */ |
||
| 982 | final public function getLastError() { |
||
| 1004 | |||
| 1005 | /** |
||
| 1006 | * Clear the "last error" registry |
||
| 1007 | */ |
||
| 1008 | final public function clearLastError() { |
||
| 1012 | |||
| 1013 | /** |
||
| 1014 | * Clear the in-process caches; useful for testing |
||
| 1015 | * |
||
| 1016 | * @since 1.27 |
||
| 1017 | */ |
||
| 1018 | public function clearProcessCache() { |
||
| 1021 | |||
| 1022 | /** |
||
| 1023 | * @param integer $flag ATTR_* class constant |
||
| 1024 | * @return integer QOS_* class constant |
||
| 1025 | * @since 1.28 |
||
| 1026 | */ |
||
| 1027 | public function getQoS( $flag ) { |
||
| 1030 | |||
| 1031 | /** |
||
| 1032 | * Do the actual async bus purge of a key |
||
| 1033 | * |
||
| 1034 | * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>" |
||
| 1035 | * |
||
| 1036 | * @param string $key Cache key |
||
| 1037 | * @param integer $ttl How long to keep the tombstone [seconds] |
||
| 1038 | * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key |
||
| 1039 | * @return bool Success |
||
| 1040 | */ |
||
| 1041 | protected function relayPurge( $key, $ttl, $holdoff ) { |
||
| 1057 | |||
| 1058 | /** |
||
| 1059 | * Do the actual async bus delete of a key |
||
| 1060 | * |
||
| 1061 | * @param string $key Cache key |
||
| 1062 | * @return bool Success |
||
| 1063 | */ |
||
| 1064 | protected function relayDelete( $key ) { |
||
| 1077 | |||
| 1078 | /** |
||
| 1079 | * Check if a key should be regenerated (using random probability) |
||
| 1080 | * |
||
| 1081 | * This returns false if $curTTL >= $lowTTL. Otherwise, the chance |
||
| 1082 | * of returning true increases steadily from 0% to 100% as the $curTTL |
||
| 1083 | * moves from $lowTTL to 0 seconds. This handles widely varying |
||
| 1084 | * levels of cache access traffic. |
||
| 1085 | * |
||
| 1086 | * @param float $curTTL Approximate TTL left on the key if present |
||
| 1087 | * @param float $lowTTL Consider a refresh when $curTTL is less than this |
||
| 1088 | * @return bool |
||
| 1089 | */ |
||
| 1090 | protected function worthRefresh( $curTTL, $lowTTL ) { |
||
| 1101 | |||
| 1102 | /** |
||
| 1103 | * Check whether $value is appropriately versioned and not older than $minTime (if set) |
||
| 1104 | * |
||
| 1105 | * @param array $value |
||
| 1106 | * @param bool $versioned |
||
| 1107 | * @param float $asOf The time $value was generated |
||
| 1108 | * @param float $minTime The last time the main value was generated (0.0 if unknown) |
||
| 1109 | * @return bool |
||
| 1110 | */ |
||
| 1111 | protected function isValid( $value, $versioned, $asOf, $minTime ) { |
||
| 1120 | |||
| 1121 | /** |
||
| 1122 | * Do not use this method outside WANObjectCache |
||
| 1123 | * |
||
| 1124 | * @param mixed $value |
||
| 1125 | * @param integer $ttl [0=forever] |
||
| 1126 | * @param float $now Unix Current timestamp just before calling set() |
||
| 1127 | * @return array |
||
| 1128 | */ |
||
| 1129 | protected function wrap( $value, $ttl, $now ) { |
||
| 1137 | |||
| 1138 | /** |
||
| 1139 | * Do not use this method outside WANObjectCache |
||
| 1140 | * |
||
| 1141 | * @param array|string|bool $wrapped |
||
| 1142 | * @param float $now Unix Current timestamp (preferrably pre-query) |
||
| 1143 | * @return array (mixed; false if absent/invalid, current time left) |
||
| 1144 | */ |
||
| 1145 | protected function unwrap( $wrapped, $now ) { |
||
| 1177 | |||
| 1178 | /** |
||
| 1179 | * @param array $keys |
||
| 1180 | * @param string $prefix |
||
| 1181 | * @return string[] |
||
| 1182 | */ |
||
| 1183 | protected static function prefixCacheKeys( array $keys, $prefix ) { |
||
| 1191 | |||
| 1192 | /** |
||
| 1193 | * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>" |
||
| 1194 | * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer), |
||
| 1195 | * or false if value isn't a valid purge value |
||
| 1196 | */ |
||
| 1197 | protected static function parsePurgeValue( $value ) { |
||
| 1216 | |||
| 1217 | /** |
||
| 1218 | * @param float $timestamp |
||
| 1219 | * @param int $holdoff In seconds |
||
| 1220 | * @return string Wrapped purge value |
||
| 1221 | */ |
||
| 1222 | protected function makePurgeValue( $timestamp, $holdoff ) { |
||
| 1225 | } |
||
| 1226 |