Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.59% covered (danger)
43.59%
17 / 39
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
MemcachedBagOStuff
44.74% covered (danger)
44.74%
17 / 38
37.50% covered (danger)
37.50%
3 / 8
65.78
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeKeyInternal
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 requireConvertGenericKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateKeyEncoding
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 validateKeyAndPrependRoute
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 stripRouteFromKey
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 fixExpiry
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 doIncrWithInit
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
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 */
6namespace Wikimedia\ObjectCache;
7
8use Exception;
9use InvalidArgumentException;
10use 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 */
19abstract 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 */
184class_alias( MemcachedBagOStuff::class, 'MemcachedBagOStuff' );