} return $this->doDeleteMulti( $keys, $flags ); } /** * @param string[] $keys List of keys * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success */ protected function doDeleteMulti( array $keys, $flags = 0 ) { $res = true; foreach ( $keys as $key ) { $res = $this->doDelete( $key, $flags ) && $res; } return $res; } /** * Change the expiration of multiple keys that exist * * @param string[] $keys List of keys * @param int $exptime TTL or UNIX timestamp * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) * @return bool Success * * @since 1.34 */ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) { return $this->doChangeTTLMulti( $keys, $exptime, $flags ); } /** * @param string[] $keys List of keys * @param int $exptime TTL or UNIX timestamp * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success */ protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) { $res = true; foreach ( $keys as $key ) { $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res; } return $res; } /** * Get and reassemble the chunks of blob at the given key * * @param string $key * @param mixed $mainValue * @return string|null|bool The combined string, false if missing, null on error */ final protected function resolveSegments( $key, $mainValue ) { if ( SerializedValueContainer::isUnified( $mainValue ) ) { return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} ); } if ( SerializedValueContainer::isSegmented( $mainValue ) ) { $orderedKeys = array_map( function ( $segmentHash ) use ( $key ) { return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash ); }, $mainValue->{SerializedValueContainer::SEGMENTED_HASHES} ); $segmentsByKey = $this->doGetMulti( $orderedKeys ); $parts = []; foreach ( $orderedKeys as $segmentKey ) { if ( isset( $segmentsByKey[$segmentKey] ) ) { $parts[] = $segmentsByKey[$segmentKey]; } else { // missing segment return false; } } return $this->unserialize( implode( '', $parts ) ); } return $mainValue; } /** * Check if a value should use a segmentation wrapper due to its size * * In order to avoid extra serialization and/or twice-serialized wrappers, just check if * the value is a large string. Support cache wrappers (e.g. WANObjectCache) that use 2D * arrays to wrap values. This does not recurse in order to avoid overhead from complex * structures and the risk of infinite loops (due to references). * * @param mixed $value * @param int $flags * @return bool */ private function useSegmentationWrapper( $value, $flags ) { if ( $this->segmentationSize === INF || !$this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) { return false; } if ( is_string( $value ) ) { return ( strlen( $value ) >= $this->segmentationSize ); } if ( is_array( $value ) ) { // Expect that the contained value will be one of the first array entries foreach ( array_slice( $value, 0, 4 ) as $v ) { if ( is_string( $v ) && strlen( $v ) >= $this->segmentationSize ) { return true; } } } // Avoid breaking functions for incrementing/decrementing integer key values return false; } /** * Make the entry to store at a key (inline or segment list), storing any segments * * @param string $key * @param mixed $value * @param int $exptime * @param int $flags * @param mixed|null &$ok Whether the entry is usable (e.g. no missing segments) [returned] * @return mixed The entry (inline value, wrapped inline value, or wrapped segment list) * @since 1.34 */ final protected function makeValueOrSegmentList( $key, $value, $exptime, $flags, &$ok ) { $entry = $value; $ok = true; if ( $this->useSegmentationWrapper( $value, $flags ) ) { $segmentSize = $this->segmentationSize; $maxTotalSize = $this->segmentedValueMaxSize; $serialized = $this->getSerialized( $value, $key ); $size = strlen( $serialized ); if ( $size > $maxTotalSize ) { $this->logger->warning( "Value for {key} exceeds $maxTotalSize bytes; cannot segment.", [ 'key' => $key ] ); } else { // Split the serialized value into chunks and store them at different keys $chunksByKey = []; $segmentHashes = []; $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); for ( $i = 0; $i < $count; ++$i ) { $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); $hash = sha1( $segment ); $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); $chunksByKey[$chunkKey] = $segment; $segmentHashes[] = $hash; } $flags &= ~self::WRITE_ALLOW_SEGMENTS; $ok = $this->setMulti( $chunksByKey, $exptime, $flags ); $entry = SerializedValueContainer::newSegmented( $segmentHashes ); } } return $entry; } /** * @param int|float $exptime * @return bool Whether the expiry is non-infinite, and, negative or not a UNIX timestamp * @since 1.34 */ final protected function isRelativeExpiration( $exptime ) { return ( $exptime !== self::TTL_INDEFINITE && $exptime < ( 10 * self::TTL_YEAR ) ); } /** * Convert an optionally relative timestamp to an absolute time * * The input value will be cast to an integer and interpreted as follows: * - zero: no expiry; return zero (e.g. TTL_INDEFINITE) * - negative: relative TTL; return UNIX timestamp offset by this value * - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value * - positive (>= 10 years): absolute UNIX timestamp; return this value * * @param int $exptime * @return int Expiration timestamp or TTL_INDEFINITE for indefinite * @since 1.34 */ final protected function getExpirationAsTimestamp( $exptime ) { if ( $exptime == self::TTL_INDEFINITE ) { return $exptime; } return $this->isRelativeExpiration( $exptime ) ? intval( $this->getCurrentTime() + $exptime ) : $exptime; } /** * Convert an optionally absolute expiry time to a relative time. If an * absolute time is specified which is in the past, use a short expiry time. * * The input value will be cast to an integer and interpreted as follows: * - zero: no expiry; return zero (e.g. TTL_INDEFINITE) * - negative: relative TTL; return a short expiry time (1 second) * - positive (< 10 years): relative TTL; return this value * - positive (>= 10 years): absolute UNIX timestamp; return offset to current time * * @param int $exptime * @return int Relative TTL or TTL_INDEFINITE for indefinite * @since 1.34 */ final protected function getExpirationAsTTL( $exptime ) { if ( $exptime == self::TTL_INDEFINITE ) { return $exptime; } return $this->isRelativeExpiration( $exptime ) ? $exptime : (int)max( $exptime - $this->getCurrentTime(), 1 ); } /** * Check if a value is an integer * * @param mixed $value * @return bool */ final protected function isInteger( $value ) { if ( is_int( $value ) ) { return true; } elseif ( !is_string( $value ) ) { return false; } $integer = (int)$value; return ( $value === (string)$integer ); } public function getQoS( $flag ) { return $this->attrMap[$flag] ?? self::QOS_UNKNOWN; } public function getSegmentationSize() { return $this->segmentationSize; } public function getSegmentedValueMaxSize() { return $this->segmentedValueMaxSize; } /** * Get the serialized form a value, logging a warning if it involves custom classes * * @param mixed $value * @param string $key * @return string|int String/integer representation of value * @since 1.35 */ protected function getSerialized( $value, $key ) { $this->checkValueSerializability( $value, $key ); return $this->serialize( $value ); } /** * Log if a new cache value does not appear suitable for serialization at a quick glance * * This aids migration of values to JSON-like structures and the debugging of exceptions * due to serialization failure. * * This does not recurse more than one level into container structures. * * A proper cache key value is one of the following: * - null * - a scalar * - an array with scalar/null values * - an array tree with scalar/null "leaf" values * - an stdClass instance with scalar/null field values * - an stdClass instance tree with scalar/null "leaf" values * - an instance of a class that implements JsonSerializable * * @param mixed $value Result of the value generation callback for the key * @param string $key Cache key */ private function checkValueSerializability( $value, $key ) { if ( is_array( $value ) ) { $this->checkIterableMapSerializability( $value, $key ); } elseif ( is_object( $value ) ) { // Note that Closure instances count as objects if ( $value instanceof stdClass ) { $this->checkIterableMapSerializability( $value, $key ); } elseif ( !( $value instanceof JsonSerializable ) ) { $this->logger->warning( "{class} value for '{cachekey}'; serialization is suspect.", [ 'cachekey' => $key, 'class' => get_class( $value ) ] ); } } } /** * @param array|stdClass $value Result of the value generation callback for the key * @param string $key Cache key */ private function checkIterableMapSerializability( $value, $key ) { foreach ( $value as $index => $entry ) { if ( is_object( $entry ) ) { // Note that Closure instances count as objects if ( !( $entry instanceof stdClass ) && !( $entry instanceof JsonSerializable ) ) { $this->logger->warning( "{class} value for '{cachekey}' at '$index'; serialization is suspect.", [ 'cachekey' => $key, 'class' => get_class( $entry ) ] ); return; } } } } /** * @param mixed $value * @return string|int|false String/integer representation * @note Special handling is usually needed for integers so incr()/decr() work */ protected function serialize( $value ) { return is_int( $value ) ? $value : serialize( $value ); } /** * @param string|int|false $value * @return mixed Original value or false on error * @note Special handling is usually needed for integers so incr()/decr() work */ protected function unserialize( $value ) { return $this->isInteger( $value ) ? (int)$value : unserialize( $value ); } /** * @param string $text */ protected function debug( $text ) { $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] ); } /** * @param string $op Operation name as a MediumSpecificBagOStuff::METRIC_OP_* constant * @param array|array $keyInfo Key list, if payload sizes are not * applicable, otherwise, map of (key => (send payload size, receive payload size)); send * and receive sizes are 0 where not applicable and receive sizes are "false" for keys * that were not found during read operations */ protected function updateOpStats( string $op, array $keyInfo ) { $deltasByMetric = []; foreach ( $keyInfo as $indexOrKey => $keyOrSizes ) { if ( is_array( $keyOrSizes ) ) { $key = $indexOrKey; [ $sPayloadSize, $rPayloadSize ] = $keyOrSizes; } else { $key = $keyOrSizes; $sPayloadSize = 0; $rPayloadSize = 0; } // Metric prefix for the cache wrapper and key collection name $prefix = $this->determineKeyPrefixForStats( $key ); if ( $op === self::METRIC_OP_GET ) { // This operation was either a "hit" or "miss" for this key $name = "{$prefix}.{$op}_" . ( $rPayloadSize === false ? 'miss_rate' : 'hit_rate' ); } else { // There is no concept of "hit" or "miss" for this operation $name = "{$prefix}.{$op}_call_rate"; } $deltasByMetric[$name] = ( $deltasByMetric[$name] ?? 0 ) + 1; if ( $sPayloadSize > 0 ) { $name = "{$prefix}.{$op}_bytes_sent"; $deltasByMetric[$name] = ( $deltasByMetric[$name] ?? 0 ) + $sPayloadSize; } if ( $rPayloadSize > 0 ) { $name = "{$prefix}.{$op}_bytes_read"; $deltasByMetric[$name] = ( $deltasByMetric[$name] ?? 0 ) + $rPayloadSize; } } foreach ( $deltasByMetric as $name => $delta ) { $this->stats->updateCount( $name, $delta ); } } }