Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
43.59% |
17 / 39 |
|
37.50% |
3 / 8 |
CRAP | |
0.00% |
0 / 1 |
| MemcachedBagOStuff | |
44.74% |
17 / 38 |
|
37.50% |
3 / 8 |
65.78 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| makeKeyInternal | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
| requireConvertGenericKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| validateKeyEncoding | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| validateKeyAndPrependRoute | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| stripRouteFromKey | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| fixExpiry | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| doIncrWithInit | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| doIncrWithInitAsync | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| doIncrWithInitSync | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | namespace Wikimedia\ObjectCache; |
| 7 | |
| 8 | use Exception; |
| 9 | use InvalidArgumentException; |
| 10 | use RuntimeException; |
| 11 | |
| 12 | /** |
| 13 | * Store data in a memcached server or memcached cluster. |
| 14 | * |
| 15 | * This is a base class for MemcachedPhpBagOStuff and MemcachedPeclBagOStuff. |
| 16 | * |
| 17 | * @ingroup Cache |
| 18 | */ |
| 19 | abstract class MemcachedBagOStuff extends MediumSpecificBagOStuff { |
| 20 | /** @var string Routing prefix appended to keys during operations */ |
| 21 | protected $routingPrefix; |
| 22 | |
| 23 | /** |
| 24 | * @param array $params Additional parameters include: |
| 25 | * - routingPrefix: a routing prefix of the form "<datacenter>/<cluster>/" used to convey |
| 26 | * the location/strategy to use for handling keys accessed from this instance. The prefix |
| 27 | * is prepended to keys during cache operations. The memcached proxy must preserve these |
| 28 | * prefixes in any responses that include requested keys (e.g. get/gets). The proxy is |
| 29 | * also assumed to strip the routing prefix from the stored key name, which allows for |
| 30 | * unprefixed access. This can be used with mcrouter. [optional] |
| 31 | */ |
| 32 | public function __construct( array $params ) { |
| 33 | $params['segmentationSize'] ??= 917_504; // < 1MiB |
| 34 | parent::__construct( $params ); |
| 35 | |
| 36 | $this->routingPrefix = $params['routingPrefix'] ?? ''; |
| 37 | |
| 38 | // ...and does not use special disk-cache plugins |
| 39 | $this->attrMap[self::ATTR_DURABILITY] = self::QOS_DURABILITY_SERVICE; |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Format a cache key. |
| 44 | * |
| 45 | * @since 1.27 |
| 46 | * @see BagOStuff::makeKeyInternal |
| 47 | * |
| 48 | * @param string $keyspace |
| 49 | * @param string[]|int[]|null $components |
| 50 | * |
| 51 | * @return string |
| 52 | */ |
| 53 | protected function makeKeyInternal( $keyspace, $components ) { |
| 54 | $key = $keyspace; |
| 55 | foreach ( $components as $component ) { |
| 56 | $component = strtr( $component ?? '', ' ', '_' ); |
| 57 | |
| 58 | // Make sure %, #, and non-ASCII chars are escaped |
| 59 | $component = preg_replace_callback( |
| 60 | '/[^\x21-\x22\x24\x26-\x39\x3b-\x7e]+/', |
| 61 | static fn ( $m ) => rawurlencode( $m[0] ), |
| 62 | $component |
| 63 | ); |
| 64 | |
| 65 | $key .= ':' . $component; |
| 66 | } |
| 67 | |
| 68 | // Memcached keys have a maximum length of 250 characters. |
| 69 | // * Reserve 45 chars for prefixes used by wrappers like WANObjectCache. |
| 70 | return $this->makeFallbackKey( $key, 205 ); |
| 71 | } |
| 72 | |
| 73 | protected function requireConvertGenericKey(): bool { |
| 74 | return true; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Ensure that a key is safe to use (contains no control characters and no |
| 79 | * characters above the ASCII range.) |
| 80 | * |
| 81 | * @param string $key |
| 82 | * |
| 83 | * @return string |
| 84 | * @throws Exception |
| 85 | */ |
| 86 | public function validateKeyEncoding( $key ) { |
| 87 | if ( preg_match( '/[^\x21-\x7e]+/', $key ) ) { |
| 88 | throw new InvalidArgumentException( "Key contains invalid characters: $key" ); |
| 89 | } |
| 90 | |
| 91 | return $key; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * @param string $key |
| 96 | * |
| 97 | * @return string |
| 98 | */ |
| 99 | protected function validateKeyAndPrependRoute( $key ) { |
| 100 | $this->validateKeyEncoding( $key ); |
| 101 | |
| 102 | if ( $this->routingPrefix === '' ) { |
| 103 | return $key; |
| 104 | } |
| 105 | |
| 106 | if ( $key[0] === '/' ) { |
| 107 | throw new RuntimeException( "Key '$key' already contains a route." ); |
| 108 | } |
| 109 | |
| 110 | return $this->routingPrefix . $key; |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * @param string $key |
| 115 | * |
| 116 | * @return string |
| 117 | */ |
| 118 | protected function stripRouteFromKey( $key ) { |
| 119 | if ( $this->routingPrefix === '' ) { |
| 120 | return $key; |
| 121 | } |
| 122 | |
| 123 | if ( str_starts_with( $key, $this->routingPrefix ) ) { |
| 124 | return substr( $key, strlen( $this->routingPrefix ) ); |
| 125 | } |
| 126 | |
| 127 | return $key; |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param int|float $exptime |
| 132 | * |
| 133 | * @return int |
| 134 | */ |
| 135 | protected function fixExpiry( $exptime ) { |
| 136 | if ( $exptime < 0 ) { |
| 137 | // The PECL driver does not seem to like negative relative values |
| 138 | $expiresAt = $this->getCurrentTime() + $exptime; |
| 139 | } elseif ( $this->isRelativeExpiration( $exptime ) ) { |
| 140 | // TTLs higher than 30 days will be detected as absolute TTLs |
| 141 | // (UNIX timestamps), and will result in the cache entry being |
| 142 | // discarded immediately because the expiry is in the past. |
| 143 | // Clamp expires >30d at 30d, unless they're >=1e9 in which |
| 144 | // case they are likely to really be absolute (1e9 = 2011-09-09) |
| 145 | $expiresAt = min( $exptime, self::TTL_MONTH ); |
| 146 | } else { |
| 147 | $expiresAt = $exptime; |
| 148 | } |
| 149 | |
| 150 | return (int)$expiresAt; |
| 151 | } |
| 152 | |
| 153 | /** @inheritDoc */ |
| 154 | protected function doIncrWithInit( $key, $exptime, $step, $init, $flags ) { |
| 155 | if ( $flags & self::WRITE_BACKGROUND ) { |
| 156 | return $this->doIncrWithInitAsync( $key, $exptime, $step, $init ); |
| 157 | } else { |
| 158 | return $this->doIncrWithInitSync( $key, $exptime, $step, $init ); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * @param string $key |
| 164 | * @param int $exptime |
| 165 | * @param int $step |
| 166 | * @param int $init |
| 167 | * |
| 168 | * @return bool True on success, false on failure |
| 169 | */ |
| 170 | abstract protected function doIncrWithInitAsync( $key, $exptime, $step, $init ); |
| 171 | |
| 172 | /** |
| 173 | * @param string $key |
| 174 | * @param int $exptime |
| 175 | * @param int $step |
| 176 | * @param int $init |
| 177 | * |
| 178 | * @return int|bool New value or false on failure |
| 179 | */ |
| 180 | abstract protected function doIncrWithInitSync( $key, $exptime, $step, $init ); |
| 181 | } |
| 182 | |
| 183 | /** @deprecated class alias since 1.43 */ |
| 184 | class_alias( MemcachedBagOStuff::class, 'MemcachedBagOStuff' ); |