Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.41% covered (danger)
19.41%
73 / 376
13.04% covered (danger)
13.04%
6 / 46
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediumSpecificBagOStuff
19.47% covered (danger)
19.47%
73 / 375
13.04% covered (danger)
13.04%
6 / 46
10823.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 trackDuplicateKeys
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
21.27
 doGet
n/a
0 / 0
n/a
0 / 0
0
 set
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 doSet
n/a
0 / 0
n/a
0 / 0
0
 delete
14.29% covered (danger)
14.29%
2 / 14
0.00% covered (danger)
0.00%
0 / 1
14.08
 doDelete
n/a
0 / 0
n/a
0 / 0
0
 add
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 doAdd
n/a
0 / 0
n/a
0 / 0
0
 merge
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mergeViaCas
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 cas
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 doCas
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 tokensMatch
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 changeTTL
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doChangeTTL
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 incrWithInit
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 doIncrWithInit
n/a
0 / 0
n/a
0 / 0
0
 lock
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 doLock
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 unlock
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 doUnlock
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 makeLockKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteObjectsExpiringBefore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMulti
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 doGetMulti
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setMulti
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 doSetMulti
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 deleteMulti
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 doDeleteMulti
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 changeTTLMulti
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doChangeTTLMulti
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 resolveSegments
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
14.42
 useSegmentationWrapper
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
29.95
 makeValueOrSegmentList
16.00% covered (danger)
16.00%
4 / 25
0.00% covered (danger)
0.00%
0 / 1
19.82
 isRelativeExpiration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getExpirationAsTimestamp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getExpirationAsTTL
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isInteger
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getQoS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSegmentationSize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSegmentedValueMaxSize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSerialized
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkValueSerializability
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 checkIterableMapSerializability
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 serialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 unserialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 debug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 determinekeyGroupForStats
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 updateOpStats
89.36% covered (warning)
89.36%
42 / 47
0.00% covered (danger)
0.00%
0 / 1
8.08
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace Wikimedia\ObjectCache;
21
22use InvalidArgumentException;
23use JsonSerializable;
24use stdClass;
25use Wikimedia\ObjectCache\Serialized\SerializedValueContainer;
26use Wikimedia\WaitConditionLoop;
27
28/**
29 * Helper classs that implements most of BagOStuff for a backend.
30 *
31 * This should be used by concrete implementations only. Wrapper classes that
32 * proxy another BagOStuff should extend and implement BagOStuff directly.
33 *
34 * @ingroup Cache
35 * @since 1.34
36 */
37abstract class MediumSpecificBagOStuff extends BagOStuff {
38    /** @var array<string,array> Map of (key => (class LOCK_* constant => value) */
39    protected $locks = [];
40    /** @var int Bytes; chunk size of segmented cache values */
41    protected $segmentationSize;
42    /** @var int Bytes; maximum total size of a segmented cache value */
43    protected $segmentedValueMaxSize;
44
45    /** @var float Seconds; maximum expected seconds for a lock ping to reach the backend */
46    protected $maxLockSendDelay = 0.05;
47
48    /** @var array */
49    private $duplicateKeyLookups = [];
50    /** @var bool */
51    private $reportDupes = false;
52    /** @var bool */
53    private $dupeTrackScheduled = false;
54
55    /** Component to use for key construction of blob segment keys */
56    private const SEGMENT_COMPONENT = 'segment';
57
58    /** Idiom for doGet() to return extra information by reference */
59    protected const PASS_BY_REF = -1;
60
61    protected const METRIC_OP_GET = 'get';
62    protected const METRIC_OP_SET = 'set';
63    protected const METRIC_OP_DELETE = 'delete';
64    protected const METRIC_OP_CHANGE_TTL = 'change_ttl';
65    protected const METRIC_OP_ADD = 'add';
66    protected const METRIC_OP_INCR = 'incr';
67    protected const METRIC_OP_DECR = 'decr';
68    protected const METRIC_OP_CAS = 'cas';
69
70    protected const LOCK_RCLASS = 0;
71    protected const LOCK_DEPTH = 1;
72    protected const LOCK_TIME = 2;
73    protected const LOCK_EXPIRY = 3;
74
75    /**
76     * @see BagOStuff::__construct()
77     * Additional $params options include:
78     *   - logger: Psr\Log\LoggerInterface instance
79     *   - reportDupes: Whether to emit warning log messages for all keys that were
80     *      requested more than once (requires an asyncHandler).
81     *   - segmentationSize: The chunk size, in bytes, of segmented values. The value should
82     *      not exceed the maximum size of values in the storage backend, as configured by
83     *      the site administrator.
84     *   - segmentedValueMaxSize: The maximum total size, in bytes, of segmented values.
85     *      This should be configured to a reasonable size give the site traffic and the
86     *      amount of I/O between application and cache servers that the network can handle.
87     *
88     * @param array $params
89     *
90     * @phpcs:ignore Generic.Files.LineLength
91     * @phan-param array{logger?:\Psr\Log\LoggerInterface,asyncHandler?:callable,reportDupes?:bool,segmentationSize?:int|float,segmentedValueMaxSize?:int} $params
92     */
93    public function __construct( array $params = [] ) {
94        parent::__construct( $params );
95
96        if ( !empty( $params['reportDupes'] ) && $this->asyncHandler ) {
97            $this->reportDupes = true;
98        }
99
100        // Default to 8MiB if segmentationSize is not set
101        $this->segmentationSize = $params['segmentationSize'] ?? 8_388_608;
102        // Default to 64MiB if segmentedValueMaxSize is not set
103        $this->segmentedValueMaxSize = $params['segmentedValueMaxSize'] ?? 67_108_864;
104    }
105
106    /**
107     * Get an item with the given key
108     *
109     * If the key includes a deterministic input hash (e.g. the key can only have
110     * the correct value) or complete staleness checks are handled by the caller
111     * (e.g. nothing relies on the TTL), then the READ_VERIFIED flag should be set.
112     * This lets tiered backends know they can safely upgrade a cached value to
113     * higher tiers using standard TTLs.
114     *
115     * @param string $key
116     * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
117     *
118     * @return mixed Returns false on failure or if the item does not exist
119     */
120    public function get( $key, $flags = 0 ) {
121        $this->trackDuplicateKeys( $key );
122
123        return $this->resolveSegments( $key, $this->doGet( $key, $flags ) );
124    }
125
126    /**
127     * Track the number of times that a given key has been used.
128     *
129     * @param string $key
130     */
131    private function trackDuplicateKeys( $key ) {
132        if ( !$this->reportDupes ) {
133            return;
134        }
135
136        if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
137            // Track that we have seen this key. This N-1 counting style allows
138            // easy filtering with array_filter() later.
139            $this->duplicateKeyLookups[$key] = 0;
140        } else {
141            $this->duplicateKeyLookups[$key]++;
142
143            if ( $this->dupeTrackScheduled === false ) {
144                $this->dupeTrackScheduled = true;
145                // Schedule a callback that logs keys processed more than once by get().
146                call_user_func( $this->asyncHandler, function () {
147                    $dups = array_filter( $this->duplicateKeyLookups );
148                    foreach ( $dups as $key => $count ) {
149                        $this->logger->warning(
150                            'Duplicate get(): "{key}" fetched {count} times',
151                            // Count is N-1 of the actual lookup count
152                            [ 'key' => $key, 'count' => $count + 1, ]
153                        );
154                    }
155                } );
156            }
157        }
158    }
159
160    /**
161     * Get an item
162     *
163     * The CAS token should be null if the key does not exist or the value is corrupt
164     *
165     * @param string $key
166     * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
167     * @param mixed &$casToken CAS token if MediumSpecificBagOStuff::PASS_BY_REF [returned]
168     *
169     * @return mixed Returns false on failure or if the item does not exist
170     */
171    abstract protected function doGet( $key, $flags = 0, &$casToken = null );
172
173    public function set( $key, $value, $exptime = 0, $flags = 0 ) {
174        $entry = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags, $ok );
175
176        // Only when all segments (if any) are stored should the main key be changed
177        return $ok && $this->doSet( $key, $entry, $exptime, $flags );
178    }
179
180    /**
181     * Set an item
182     *
183     * @param string $key
184     * @param mixed $value
185     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
186     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
187     *
188     * @return bool Success
189     */
190    abstract protected function doSet( $key, $value, $exptime = 0, $flags = 0 );
191
192    public function delete( $key, $flags = 0 ) {
193        if ( !$this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
194            return $this->doDelete( $key, $flags );
195        }
196
197        $mainValue = $this->doGet( $key, self::READ_LATEST );
198        if ( !$this->doDelete( $key, $flags ) ) {
199            return false;
200        }
201
202        if ( !SerializedValueContainer::isSegmented( $mainValue ) ) {
203            // no segments to delete
204            return true;
205        }
206
207        $orderedKeys = array_map(
208            function ( $segmentHash ) use ( $key ) {
209                return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
210            },
211            $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
212        );
213
214        return $this->deleteMulti( $orderedKeys, $flags & ~self::WRITE_ALLOW_SEGMENTS );
215    }
216
217    /**
218     * Delete an item
219     *
220     * @param string $key
221     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
222     *
223     * @return bool True if the item was deleted or not found, false on failure
224     */
225    abstract protected function doDelete( $key, $flags = 0 );
226
227    public function add( $key, $value, $exptime = 0, $flags = 0 ) {
228        $entry = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags, $ok );
229
230        // Only when all segments (if any) are stored should the main key be changed
231        return $ok && $this->doAdd( $key, $entry, $exptime, $flags );
232    }
233
234    /**
235     * Insert an item if it does not already exist
236     *
237     * @param string $key
238     * @param mixed $value
239     * @param int $exptime
240     * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
241     *
242     * @return bool Success
243     */
244    abstract protected function doAdd( $key, $value, $exptime = 0, $flags = 0 );
245
246    /**
247     * Merge changes into the existing cache value (possibly creating a new one)
248     *
249     * The callback function returns the new value given the current value
250     * (which will be false if not present), and takes the arguments:
251     * (this BagOStuff, cache key, current value, TTL).
252     * The TTL parameter is reference set to $exptime. It can be overridden in the callback.
253     * Nothing is stored nor deleted if the callback returns false.
254     *
255     * @param string $key
256     * @param callable $callback Callback method to be executed
257     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
258     * @param int $attempts The amount of times to attempt a merge in case of failure
259     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
260     *
261     * @return bool Success
262     */
263    public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
264        return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
265    }
266
267    /**
268     * @param string $key
269     * @param callable $callback Callback method to be executed
270     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
271     * @param int $attempts The amount of times to attempt a merge in case of failure
272     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
273     *
274     * @return bool Success
275     * @see BagOStuff::merge()
276     */
277    final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
278        $attemptsLeft = $attempts;
279        do {
280            $token = self::PASS_BY_REF;
281            // Get the old value and CAS token from cache
282            $watchPoint = $this->watchErrors();
283            $currentValue = $this->resolveSegments(
284                $key,
285                $this->doGet( $key, $flags, $token )
286            );
287            if ( $this->getLastError( $watchPoint ) ) {
288                // Don't spam slow retries due to network problems (retry only on races)
289                $this->logger->warning(
290                    __METHOD__ . ' failed due to read I/O error on get() for {key}.', [ 'key' => $key ]
291                );
292                $success = false;
293                break;
294            }
295
296            // Derive the new value from the old value
297            $value = $callback( $this, $key, $currentValue, $exptime );
298            $keyWasNonexistent = ( $currentValue === false );
299            $valueMatchesOldValue = ( $value === $currentValue );
300            // free RAM in case the value is large
301            unset( $currentValue );
302
303            $watchPoint = $this->watchErrors();
304            if ( $value === false || $exptime < 0 ) {
305                // do nothing
306                $success = true;
307            } elseif ( $valueMatchesOldValue && $attemptsLeft !== $attempts ) {
308                // recently set by another thread to the same value
309                $success = true;
310            } elseif ( $keyWasNonexistent ) {
311                // Try to create the key, failing if it gets created in the meantime
312                $success = $this->add( $key, $value, $exptime, $flags );
313            } else {
314                // Try to update the key, failing if it gets changed in the meantime
315                $success = $this->cas( $token, $key, $value, $exptime, $flags );
316            }
317            if ( $this->getLastError( $watchPoint ) ) {
318                // Don't spam slow retries due to network problems (retry only on races)
319                $this->logger->warning(
320                    __METHOD__ . ' failed due to write I/O error for {key}.',
321                    [ 'key' => $key ]
322                );
323                $success = false;
324                break;
325            }
326
327        } while ( !$success && --$attemptsLeft );
328
329        return $success;
330    }
331
332    /**
333     * Set an item if the current CAS token matches the provided CAS token
334     *
335     * @param mixed $casToken Only set the item if it still has this CAS token
336     * @param string $key
337     * @param mixed $value
338     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
339     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
340     *
341     * @return bool Success
342     */
343    protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
344        if ( $casToken === null ) {
345            $this->logger->warning(
346                __METHOD__ . ' got empty CAS token for {key}.',
347                [ 'key' => $key ]
348            );
349
350            // caller may have meant to use add()?
351            return false;
352        }
353
354        $entry = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags, $ok );
355
356        // Only when all segments (if any) are stored should the main key be changed
357        return $ok && $this->doCas( $casToken, $key, $entry, $exptime, $flags );
358    }
359
360    /**
361     * Set an item if the current CAS token matches the provided CAS token
362     *
363     * @param mixed $casToken CAS token from an existing version of the key
364     * @param string $key
365     * @param mixed $value
366     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
367     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
368     *
369     * @return bool Success
370     */
371    protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
372        // @TODO: the use of lock() assumes that all other relevant sets() use a lock
373        if ( !$this->lock( $key, 0 ) ) {
374            // non-blocking
375            return false;
376        }
377
378        $curCasToken = self::PASS_BY_REF;
379        $watchPoint = $this->watchErrors();
380        $exists = ( $this->doGet( $key, self::READ_LATEST, $curCasToken ) !== false );
381        if ( $this->getLastError( $watchPoint ) ) {
382            // Fail if the old CAS token could not be read
383            $success = false;
384            $this->logger->warning(
385                __METHOD__ . ' failed due to write I/O error for {key}.',
386                [ 'key' => $key ]
387            );
388        } elseif ( $exists && $this->tokensMatch( $casToken, $curCasToken ) ) {
389            $success = $this->doSet( $key, $value, $exptime, $flags );
390        } else {
391            // mismatched or failed
392            $success = false;
393            $this->logger->info(
394                __METHOD__ . ' failed due to race condition for {key}.',
395                [ 'key' => $key, 'key_exists' => $exists ]
396            );
397        }
398
399        $this->unlock( $key );
400
401        return $success;
402    }
403
404    /**
405     * @param mixed $value CAS token for an existing key
406     * @param mixed $otherValue CAS token for an existing key
407     *
408     * @return bool Whether the two tokens match
409     */
410    final protected function tokensMatch( $value, $otherValue ) {
411        $type = gettype( $value );
412        // Ideally, tokens are counters, timestamps, hashes, or serialized PHP values.
413        // However, some classes might use the PHP values themselves.
414        if ( $type !== gettype( $otherValue ) ) {
415            return false;
416        }
417        // Serialize both tokens to strictly compare objects or arrays (which might objects
418        // nested inside). Note that this will not apply if integer/string CAS tokens are used.
419        if ( $type === 'array' || $type === 'object' ) {
420            return ( serialize( $value ) === serialize( $otherValue ) );
421        }
422
423        // For string/integer tokens, use a simple comparison
424        return ( $value === $otherValue );
425    }
426
427    /**
428     * Change the expiration on a key if it exists
429     *
430     * If an expiry in the past is given then the key will immediately be expired
431     *
432     * For large values written using WRITE_ALLOW_SEGMENTS, this only changes the TTL of the
433     * main segment list key. While lowering the TTL of the segment list key has the effect of
434     * functionally lowering the TTL of the key, it might leave unused blobs in cache for longer.
435     * Raising the TTL of such keys is not effective, since the expiration of a single segment
436     * key effectively expires the entire value.
437     *
438     * @param string $key
439     * @param int $exptime TTL or UNIX timestamp
440     * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
441     *
442     * @return bool Success Returns false on failure or if the item does not exist
443     * @since 1.28
444     */
445    public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
446        return $this->doChangeTTL( $key, $exptime, $flags );
447    }
448
449    /**
450     * @param string $key
451     * @param int $exptime
452     * @param int $flags
453     *
454     * @return bool
455     */
456    protected function doChangeTTL( $key, $exptime, $flags ) {
457        // @TODO: the use of lock() assumes that all other relevant sets() use a lock
458        if ( !$this->lock( $key, 0 ) ) {
459            return false;
460        }
461
462        $expiry = $this->getExpirationAsTimestamp( $exptime );
463        $delete = ( $expiry != self::TTL_INDEFINITE && $expiry < $this->getCurrentTime() );
464
465        // Use doGet() to avoid having to trigger resolveSegments()
466        $blob = $this->doGet( $key, self::READ_LATEST );
467        if ( $blob ) {
468            if ( $delete ) {
469                $ok = $this->doDelete( $key, $flags );
470            } else {
471                $ok = $this->doSet( $key, $blob, $exptime, $flags );
472            }
473        } else {
474            $ok = false;
475        }
476
477        $this->unlock( $key );
478
479        return $ok;
480    }
481
482    public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
483        $step = (int)$step;
484        $init = is_int( $init ) ? $init : $step;
485
486        return $this->doIncrWithInit( $key, $exptime, $step, $init, $flags );
487    }
488
489    /**
490     * @param string $key
491     * @param int $exptime
492     * @param int $step
493     * @param int $init
494     * @param int $flags
495     *
496     * @return int|bool New value or false on failure
497     */
498    abstract protected function doIncrWithInit( $key, $exptime, $step, $init, $flags );
499
500    /**
501     * @param string $key
502     * @param int $timeout
503     * @param int $exptime
504     * @param string $rclass
505     *
506     * @return bool
507     */
508    public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
509        $exptime = min( $exptime ?: INF, self::TTL_DAY );
510
511        $acquired = false;
512
513        if ( isset( $this->locks[$key] ) ) {
514            // Already locked; avoid deadlocks and allow lock reentry if specified
515            if ( $rclass != '' && $this->locks[$key][self::LOCK_RCLASS] === $rclass ) {
516                ++$this->locks[$key][self::LOCK_DEPTH];
517                $acquired = true;
518            }
519        } else {
520            // Not already locked; acquire a lock on the backend
521            $lockTsUnix = $this->doLock( $key, $timeout, $exptime );
522            if ( $lockTsUnix !== null ) {
523                $this->locks[$key] = [
524                    self::LOCK_RCLASS => $rclass,
525                    self::LOCK_DEPTH  => 1,
526                    self::LOCK_TIME   => $lockTsUnix,
527                    self::LOCK_EXPIRY => $lockTsUnix + $exptime
528                ];
529                $acquired = true;
530            }
531        }
532
533        return $acquired;
534    }
535
536    /**
537     * @see MediumSpecificBagOStuff::lock()
538     *
539     * @param string $key
540     * @param int $timeout Lock wait timeout; 0 for non-blocking [optional]
541     * @param int $exptime Lock time-to-live 1 day maximum [optional]
542     *
543     * @return float|null UNIX timestamp of acquisition; null on failure
544     */
545    protected function doLock( $key, $timeout, $exptime ) {
546        $lockTsUnix = null;
547
548        $fname = __METHOD__;
549        $loop = new WaitConditionLoop(
550            function () use ( $key, $exptime, $fname, &$lockTsUnix ) {
551                $watchPoint = $this->watchErrors();
552                if ( $this->add( $this->makeLockKey( $key ), 1, $exptime ) ) {
553                    $lockTsUnix = microtime( true );
554
555                    return WaitConditionLoop::CONDITION_REACHED;
556                } elseif ( $this->getLastError( $watchPoint ) ) {
557                    $this->logger->warning(
558                        "$fname failed due to I/O error for {key}.",
559                        [ 'key' => $key ]
560                    );
561
562                    return WaitConditionLoop::CONDITION_ABORTED;
563                }
564
565                return WaitConditionLoop::CONDITION_CONTINUE;
566            },
567            $timeout
568        );
569        $code = $loop->invoke();
570
571        if ( $code === $loop::CONDITION_TIMED_OUT ) {
572            $this->logger->warning(
573                "$fname failed due to timeout for {key}.",
574                [ 'key' => $key, 'timeout' => $timeout ]
575            );
576        }
577
578        return $lockTsUnix;
579    }
580
581    /**
582     * Release an advisory lock on a key string
583     *
584     * @param string $key
585     *
586     * @return bool Success
587     */
588    public function unlock( $key ) {
589        $released = false;
590
591        if ( isset( $this->locks[$key] ) ) {
592            if ( --$this->locks[$key][self::LOCK_DEPTH] > 0 ) {
593                $released = true;
594            } else {
595                $released = $this->doUnlock( $key );
596                unset( $this->locks[$key] );
597                if ( !$released ) {
598                    $this->logger->warning(
599                        __METHOD__ . ' failed to release lock for {key}.',
600                        [ 'key' => $key ]
601                    );
602                }
603            }
604        } else {
605            $this->logger->warning(
606                __METHOD__ . ' no lock to release for {key}.',
607                [ 'key' => $key ]
608            );
609        }
610
611        return $released;
612    }
613
614    /**
615     * @see MediumSpecificBagOStuff::unlock()
616     *
617     * @param string $key
618     *
619     * @return bool Success
620     */
621    protected function doUnlock( $key ) {
622        $released = false;
623
624        // Estimate the remaining TTL of the lock key
625        $curTTL = $this->locks[$key][self::LOCK_EXPIRY] - $this->getCurrentTime();
626
627        // Check the risk of race conditions for key deletion
628        if ( $this->getQoS( self::ATTR_DURABILITY ) <= self::QOS_DURABILITY_SCRIPT ) {
629            // Lock (and data) keys use memory specific to this request (e.g. HashBagOStuff)
630            $isSafe = true;
631        } else {
632            // It is unsafe to delete the lock key if there is a serious risk of the key already
633            // being claimed by another thread before the delete operation reaches the backend
634            $isSafe = ( $curTTL > $this->maxLockSendDelay );
635        }
636
637        if ( $isSafe ) {
638            $released = $this->doDelete( $this->makeLockKey( $key ) );
639        } else {
640            $this->logger->warning(
641                "Lock for {key} held too long ({age} sec).",
642                [ 'key' => $key, 'curTTL' => $curTTL ]
643            );
644        }
645
646        return $released;
647    }
648
649    /**
650     * @param string $key
651     *
652     * @return string
653     */
654    protected function makeLockKey( $key ) {
655        return "$key:lock";
656    }
657
658    public function deleteObjectsExpiringBefore(
659        $timestamp,
660        ?callable $progress = null,
661        $limit = INF,
662        ?string $tag = null
663    ) {
664        return false;
665    }
666
667    /**
668     * Get an associative array containing the item for each of the keys that have items.
669     *
670     * @param string[] $keys List of keys; can be a map of (unused => key) for convenience
671     * @param int $flags Bitfield; supports READ_LATEST [optional]
672     *
673     * @return mixed[] Map of (key => value) for existing keys; preserves the order of $keys
674     */
675    public function getMulti( array $keys, $flags = 0 ) {
676        $foundByKey = $this->doGetMulti( $keys, $flags );
677
678        $res = [];
679        foreach ( $keys as $key ) {
680            // Resolve one blob at a time (avoids too much I/O at once)
681            if ( array_key_exists( $key, $foundByKey ) ) {
682                // A value should not appear in the key if a segment is missing
683                $value = $this->resolveSegments( $key, $foundByKey[$key] );
684                if ( $value !== false ) {
685                    $res[$key] = $value;
686                }
687            }
688        }
689
690        return $res;
691    }
692
693    /**
694     * Get an associative array containing the item for each of the keys that have items.
695     *
696     * @param string[] $keys List of keys
697     * @param int $flags Bitfield; supports READ_LATEST [optional]
698     *
699     * @return array Map of (key => value) for existing keys; preserves the order of $keys
700     */
701    protected function doGetMulti( array $keys, $flags = 0 ) {
702        $res = [];
703        foreach ( $keys as $key ) {
704            $val = $this->doGet( $key, $flags );
705            if ( $val !== false ) {
706                $res[$key] = $val;
707            }
708        }
709
710        return $res;
711    }
712
713    /**
714     * Batch insertion/replace
715     *
716     * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O
717     *
718     * @param mixed[] $valueByKey Map of (key => value)
719     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
720     * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
721     *
722     * @return bool Success
723     * @since 1.24
724     */
725    public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
726        if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
727            throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
728        }
729
730        return $this->doSetMulti( $valueByKey, $exptime, $flags );
731    }
732
733    /**
734     * @param mixed[] $data Map of (key => value)
735     * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
736     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
737     *
738     * @return bool Success
739     */
740    protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
741        $res = true;
742        foreach ( $data as $key => $value ) {
743            $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
744        }
745
746        return $res;
747    }
748
749    public function deleteMulti( array $keys, $flags = 0 ) {
750        if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
751            throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
752        }
753
754        return $this->doDeleteMulti( $keys, $flags );
755    }
756
757    /**
758     * @param string[] $keys List of keys
759     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
760     *
761     * @return bool Success
762     */
763    protected function doDeleteMulti( array $keys, $flags = 0 ) {
764        $res = true;
765        foreach ( $keys as $key ) {
766            $res = $this->doDelete( $key, $flags ) && $res;
767        }
768
769        return $res;
770    }
771
772    /**
773     * Change the expiration of multiple keys that exist
774     *
775     * @param string[] $keys List of keys
776     * @param int $exptime TTL or UNIX timestamp
777     * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
778     *
779     * @return bool Success
780     *
781     * @since 1.34
782     */
783    public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
784        return $this->doChangeTTLMulti( $keys, $exptime, $flags );
785    }
786
787    /**
788     * @param string[] $keys List of keys
789     * @param int $exptime TTL or UNIX timestamp
790     * @param int $flags Bitfield of BagOStuff::WRITE_* constants
791     *
792     * @return bool Success
793     */
794    protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) {
795        $res = true;
796        foreach ( $keys as $key ) {
797            $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res;
798        }
799
800        return $res;
801    }
802
803    /**
804     * Get and reassemble the chunks of blob at the given key
805     *
806     * @param string $key
807     * @param mixed $mainValue
808     *
809     * @return string|null|bool The combined string, false if missing, null on error
810     */
811    final protected function resolveSegments( $key, $mainValue ) {
812        if ( SerializedValueContainer::isSegmented( $mainValue ) ) {
813            $orderedKeys = array_map(
814                function ( $segmentHash ) use ( $key ) {
815                    return $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $segmentHash );
816                },
817                $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
818            );
819
820            $segmentsByKey = $this->doGetMulti( $orderedKeys );
821
822            $parts = [];
823            foreach ( $orderedKeys as $segmentKey ) {
824                if ( isset( $segmentsByKey[$segmentKey] ) ) {
825                    $parts[] = $segmentsByKey[$segmentKey];
826                } else {
827                    // missing segment
828                    return false;
829                }
830            }
831
832            return $this->unserialize( implode( '', $parts ) );
833        }
834
835        return $mainValue;
836    }
837
838    /**
839     * Check if a value should use a segmentation wrapper due to its size
840     *
841     * In order to avoid extra serialization and/or twice-serialized wrappers, just check if
842     * the value is a large string. Support cache wrappers (e.g. WANObjectCache) that use 2D
843     * arrays to wrap values. This does not recurse in order to avoid overhead from complex
844     * structures and the risk of infinite loops (due to references).
845     *
846     * @param mixed $value
847     * @param int $flags
848     *
849     * @return bool
850     */
851    private function useSegmentationWrapper( $value, $flags ) {
852        if (
853            $this->segmentationSize === INF ||
854            !$this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS )
855        ) {
856            return false;
857        }
858
859        if ( is_string( $value ) ) {
860            return ( strlen( $value ) >= $this->segmentationSize );
861        }
862
863        if ( is_array( $value ) ) {
864            // Expect that the contained value will be one of the first array entries
865            foreach ( array_slice( $value, 0, 4 ) as $v ) {
866                if ( is_string( $v ) && strlen( $v ) >= $this->segmentationSize ) {
867                    return true;
868                }
869            }
870        }
871
872        // Avoid breaking functions for incrementing/decrementing integer key values
873        return false;
874    }
875
876    /**
877     * Make the entry to store at a key (inline or segment list), storing any segments
878     *
879     * @param string $key
880     * @param mixed $value
881     * @param int $exptime
882     * @param int $flags
883     * @param mixed|null &$ok Whether the entry is usable (e.g. no missing segments) [returned]
884     *
885     * @return mixed The entry (inline value, wrapped inline value, or wrapped segment list)
886     * @since 1.34
887     */
888    final protected function makeValueOrSegmentList( $key, $value, $exptime, $flags, &$ok ) {
889        $entry = $value;
890        $ok = true;
891
892        if ( $this->useSegmentationWrapper( $value, $flags ) ) {
893            $segmentSize = $this->segmentationSize;
894            $maxTotalSize = $this->segmentedValueMaxSize;
895            $serialized = $this->getSerialized( $value, $key );
896            $size = strlen( $serialized );
897            if ( $size > $maxTotalSize ) {
898                $this->logger->warning(
899                    "Value for {key} exceeds $maxTotalSize bytes; cannot segment.",
900                    [ 'key' => $key ]
901                );
902            } else {
903                // Split the serialized value into chunks and store them at different keys
904                $chunksByKey = [];
905                $segmentHashes = [];
906                $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 );
907                for ( $i = 0; $i < $count; ++$i ) {
908                    $segment = substr( $serialized, $i * $segmentSize, $segmentSize );
909                    $hash = sha1( $segment );
910                    $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash );
911                    $chunksByKey[$chunkKey] = $segment;
912                    $segmentHashes[] = $hash;
913                }
914                $flags &= ~self::WRITE_ALLOW_SEGMENTS;
915                $ok = $this->setMulti( $chunksByKey, $exptime, $flags );
916                $entry = SerializedValueContainer::newSegmented( $segmentHashes );
917            }
918        }
919
920        return $entry;
921    }
922
923    /**
924     * @param int|float $exptime
925     *
926     * @return bool Whether the expiry is non-infinite, and, negative or not a UNIX timestamp
927     * @since 1.34
928     */
929    final protected function isRelativeExpiration( $exptime ) {
930        return ( $exptime !== self::TTL_INDEFINITE && $exptime < ( 10 * self::TTL_YEAR ) );
931    }
932
933    /**
934     * Convert an optionally relative timestamp to an absolute time
935     *
936     * The input value will be cast to an integer and interpreted as follows:
937     *   - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
938     *   - negative: relative TTL; return UNIX timestamp offset by this value
939     *   - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
940     *   - positive (>= 10 years): absolute UNIX timestamp; return this value
941     *
942     * @param int $exptime
943     *
944     * @return int Expiration timestamp or TTL_INDEFINITE for indefinite
945     * @since 1.34
946     */
947    final protected function getExpirationAsTimestamp( $exptime ) {
948        if ( $exptime == self::TTL_INDEFINITE ) {
949            return $exptime;
950        }
951
952        return $this->isRelativeExpiration( $exptime )
953            ? intval( $this->getCurrentTime() + $exptime )
954            : $exptime;
955    }
956
957    /**
958     * Convert an optionally absolute expiry time to a relative time. If an
959     * absolute time is specified which is in the past, use a short expiry time.
960     *
961     * The input value will be cast to an integer and interpreted as follows:
962     *   - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
963     *   - negative: relative TTL; return a short expiry time (1 second)
964     *   - positive (< 10 years): relative TTL; return this value
965     *   - positive (>= 10 years): absolute UNIX timestamp; return offset to current time
966     *
967     * @param int $exptime
968     *
969     * @return int Relative TTL or TTL_INDEFINITE for indefinite
970     * @since 1.34
971     */
972    final protected function getExpirationAsTTL( $exptime ) {
973        if ( $exptime == self::TTL_INDEFINITE ) {
974            return $exptime;
975        }
976
977        return $this->isRelativeExpiration( $exptime )
978            ? $exptime
979            : (int)max( $exptime - $this->getCurrentTime(), 1 );
980    }
981
982    /**
983     * Check if a value is an integer
984     *
985     * @param mixed $value
986     *
987     * @return bool
988     */
989    final protected function isInteger( $value ) {
990        if ( is_int( $value ) ) {
991            return true;
992        } elseif ( !is_string( $value ) ) {
993            return false;
994        }
995
996        $integer = (int)$value;
997
998        return ( $value === (string)$integer );
999    }
1000
1001    public function getQoS( $flag ) {
1002        return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
1003    }
1004
1005    /**
1006     * @deprecated since 1.43, not used anywhere.
1007     */
1008    public function getSegmentationSize() {
1009        wfDeprecated( __METHOD__, '1.43' );
1010
1011        return $this->segmentationSize;
1012    }
1013
1014    /**
1015     * @deprecated since 1.43, not used anywhere.
1016     */
1017    public function getSegmentedValueMaxSize() {
1018        wfDeprecated( __METHOD__, '1.43' );
1019
1020        return $this->segmentedValueMaxSize;
1021    }
1022
1023    /**
1024     * Get the serialized form a value, logging a warning if it involves custom classes
1025     *
1026     * @param mixed $value
1027     * @param string $key
1028     *
1029     * @return string|int String/integer representation of value
1030     * @since 1.35
1031     */
1032    protected function getSerialized( $value, $key ) {
1033        $this->checkValueSerializability( $value, $key );
1034
1035        return $this->serialize( $value );
1036    }
1037
1038    /**
1039     * Log if a new cache value does not appear suitable for serialization at a quick glance
1040     *
1041     * This aids migration of values to JSON-like structures and the debugging of exceptions
1042     * due to serialization failure.
1043     *
1044     * This does not recurse more than one level into container structures.
1045     *
1046     * A proper cache key value is one of the following:
1047     *  - null
1048     *  - a scalar
1049     *  - an array with scalar/null values
1050     *  - an array tree with scalar/null "leaf" values
1051     *  - an stdClass instance with scalar/null field values
1052     *  - an stdClass instance tree with scalar/null "leaf" values
1053     *  - an instance of a class that implements JsonSerializable
1054     *
1055     * @param mixed $value Result of the value generation callback for the key
1056     * @param string $key Cache key
1057     */
1058    private function checkValueSerializability( $value, $key ) {
1059        if ( is_array( $value ) ) {
1060            $this->checkIterableMapSerializability( $value, $key );
1061        } elseif ( is_object( $value ) ) {
1062            // Note that Closure instances count as objects
1063            if ( $value instanceof stdClass ) {
1064                $this->checkIterableMapSerializability( $value, $key );
1065            } elseif ( !( $value instanceof JsonSerializable ) ) {
1066                $this->logger->warning(
1067                    "{class} value for '{cachekey}'; serialization is suspect.",
1068                    [ 'cachekey' => $key, 'class' => get_class( $value ) ]
1069                );
1070            }
1071        }
1072    }
1073
1074    /**
1075     * @param array|stdClass $value Result of the value generation callback for the key
1076     * @param string $key Cache key
1077     */
1078    private function checkIterableMapSerializability( $value, $key ) {
1079        foreach ( $value as $index => $entry ) {
1080            if ( is_object( $entry ) ) {
1081                // Note that Closure instances count as objects
1082                if (
1083                    !( $entry instanceof \stdClass ) &&
1084                    !( $entry instanceof \JsonSerializable )
1085                ) {
1086                    $this->logger->warning(
1087                        "{class} value for '{cachekey}' at '$index'; serialization is suspect.",
1088                        [ 'cachekey' => $key, 'class' => get_class( $entry ) ]
1089                    );
1090
1091                    return;
1092                }
1093            }
1094        }
1095    }
1096
1097    /**
1098     * @param mixed $value
1099     *
1100     * @return string|int|false String/integer representation
1101     * @note Special handling is usually needed for integers so incr()/decr() work
1102     */
1103    protected function serialize( $value ) {
1104        return is_int( $value ) ? $value : serialize( $value );
1105    }
1106
1107    /**
1108     * @param string|int|false $value
1109     *
1110     * @return mixed Original value or false on error
1111     * @note Special handling is usually needed for integers so incr()/decr() work
1112     */
1113    protected function unserialize( $value ) {
1114        return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
1115    }
1116
1117    /**
1118     * @param string $text
1119     */
1120    protected function debug( $text ) {
1121        $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
1122    }
1123
1124    /**
1125     * @param string $key Key generated by BagOStuff::makeKeyInternal
1126     *
1127     * @return string A stats prefix to describe this class of key (e.g. "objectcache.file")
1128     */
1129    private function determinekeyGroupForStats( $key ): string {
1130        // Key came directly from BagOStuff::makeKey() or BagOStuff::makeGlobalKey()
1131        // and thus has the format of "<scope>:<collection>[:<constant or variable>]..."
1132        $components = explode( ':', $key, 3 );
1133        // Handle legacy callers that fail to use the key building methods
1134        $keygroup = $components[1] ?? 'UNKNOWN';
1135
1136        return strtr( $keygroup, '.', '_' );
1137    }
1138
1139    /**
1140     * @param string $op Operation name as a MediumSpecificBagOStuff::METRIC_OP_* constant
1141     * @param array<int,string>|array<string,int[]> $keyInfo Key list, if payload sizes are not
1142     *  applicable, otherwise, map of (key => (send payload size, receive payload size)); send
1143     *  and receive sizes are 0 where not applicable and receive sizes are "false" for keys
1144     *  that were not found during read operations
1145     */
1146    protected function updateOpStats( string $op, array $keyInfo ) {
1147        $deltasByMetric = [];
1148
1149        foreach ( $keyInfo as $indexOrKey => $keyOrSizes ) {
1150            if ( is_array( $keyOrSizes ) ) {
1151                $key = $indexOrKey;
1152                [ $sPayloadSize, $rPayloadSize ] = $keyOrSizes;
1153            } else {
1154                $key = $keyOrSizes;
1155                $sPayloadSize = 0;
1156                $rPayloadSize = 0;
1157            }
1158
1159            // Metric prefix for the cache wrapper and key collection name
1160            $keygroup = $this->determinekeyGroupForStats( $key );
1161
1162            if ( $op === self::METRIC_OP_GET ) {
1163                // This operation was either a "hit" or "miss" for this key
1164                if ( $rPayloadSize === false ) {
1165                    $statsdName = "objectcache.{$keygroup}.{$op}_miss_rate";
1166                    $statsName = "bagostuff_miss_total";
1167                } else {
1168                    $statsdName = "objectcache.{$keygroup}.{$op}_hit_rate";
1169                    $statsName = "bagostuff_hit_total";
1170                }
1171            } else {
1172                // There is no concept of "hit" or "miss" for this operation
1173                $statsdName = "objectcache.{$keygroup}.{$op}_call_rate";
1174                $statsName = "bagostuff_call_total";
1175            }
1176            $deltasByMetric[$statsdName] = [
1177                'delta' => ( $deltasByMetric[$statsdName]['delta'] ?? 0 ) + 1,
1178                'metric' => $statsName,
1179                'keygroup' => $keygroup,
1180                'operation' => $op,
1181            ];
1182
1183            if ( $sPayloadSize > 0 ) {
1184                $statsdName = "objectcache.{$keygroup}.{$op}_bytes_sent";
1185                $statsName = "bagostuff_bytes_sent_total";
1186                $deltasByMetric[$statsdName] = [
1187                    'delta' => ( $deltasByMetric[$statsdName]['delta'] ?? 0 ) + $sPayloadSize,
1188                    'metric' => $statsName,
1189                    'keygroup' => $keygroup,
1190                    'operation' => $op,
1191                ];
1192            }
1193
1194            if ( $rPayloadSize > 0 ) {
1195                $statsdName = "objectcache.{$keygroup}.{$op}_bytes_read";
1196                $statsName = "bagostuff_bytes_read_total";
1197                $deltasByMetric[$statsdName] = [
1198                    'delta' => ( $deltasByMetric[$statsdName]['delta'] ?? 0 ) + $rPayloadSize,
1199                    'metric' => $statsName,
1200                    'keygroup' => $keygroup,
1201                    'operation' => $op,
1202                ];
1203            }
1204        }
1205
1206        foreach ( $deltasByMetric as $statsdName => $delta ) {
1207            $this->stats->getCounter( $delta['metric'] )
1208                ->setLabel( 'keygroup', $delta['keygroup'] )
1209                ->setLabel( 'operation', $delta['operation'] )
1210                ->copyToStatsdAt( $statsdName )
1211                ->incrementBy( $delta['delta'] );
1212        }
1213    }
1214}
1215
1216/** @deprecated class alias since 1.43 */
1217class_alias( MediumSpecificBagOStuff::class, 'MediumSpecificBagOStuff' );