Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.00% covered (warning)
65.00%
39 / 60
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CachedBagOStuff
66.10% covered (warning)
66.10%
39 / 59
66.67% covered (warning)
66.67%
4 / 6
61.76
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getMulti
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 set
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 delete
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 add
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 merge
n/a
0 / 0
n/a
0 / 0
1
 changeTTL
n/a
0 / 0
n/a
0 / 0
1
 lock
n/a
0 / 0
n/a
0 / 0
1
 unlock
n/a
0 / 0
n/a
0 / 0
1
 deleteObjectsExpiringBefore
n/a
0 / 0
n/a
0 / 0
1
 setMulti
n/a
0 / 0
n/a
0 / 0
2
 deleteMulti
n/a
0 / 0
n/a
0 / 0
2
 changeTTLMulti
n/a
0 / 0
n/a
0 / 0
2
 incrWithInit
n/a
0 / 0
n/a
0 / 0
1
 setMockTime
n/a
0 / 0
n/a
0 / 0
1
 wasLastGetCached
n/a
0 / 0
n/a
0 / 0
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6namespace Wikimedia\ObjectCache;
7
8use LogicException;
9
10/**
11 * Wrap any BagOStuff and add an in-process memory cache to it.
12 *
13 * The differences between CachedBagOStuff and MultiWriteBagOStuff are:
14 * - CachedBagOStuff supports only one "backend".
15 * - There's a flag for writes to only go to the in-memory cache.
16 * - The in-memory cache is always updated.
17 * - Locks go to the backend cache (with MultiWriteBagOStuff, it would wind
18 *   up going to the HashBagOStuff used for the in-memory cache).
19 *
20 * @newable
21 * @ingroup Cache
22 */
23class CachedBagOStuff extends BagOStuff {
24    /** @var BagOStuff */
25    protected $store;
26    /** @var HashBagOStuff */
27    protected $procCache;
28
29    /**
30     * Whether the last get() call was read from cache ($procCache) or the actual store.
31     * Null means not applicable (e.g. there was no get() call yet).
32     * Should be called immediately after a get() call. getMulti() is not supported.
33     */
34    protected ?bool $wasLastGetCached = null;
35
36    /**
37     * @stable to call
38     *
39     * @param BagOStuff $backend Permanent backend to use
40     * @param array $params Parameters for HashBagOStuff
41     */
42    public function __construct( BagOStuff $backend, $params = [] ) {
43        $params['keyspace'] = $backend->keyspace;
44        parent::__construct( $params );
45
46        $this->store = $backend;
47        $this->procCache = new HashBagOStuff( $params );
48
49        $this->attrMap = $backend->attrMap;
50    }
51
52    /** @inheritDoc */
53    public function get( $key, $flags = 0 ) {
54        $value = $this->procCache->get( $key, $flags );
55        if ( $value !== false || $this->procCache->hasKey( $key ) ) {
56            $this->wasLastGetCached = true;
57            return $value;
58        }
59
60        $value = $this->store->proxyCall(
61            __FUNCTION__,
62            self::ARG0_KEY,
63            self::RES_NONKEY,
64            func_get_args(),
65            $this
66        );
67        $this->set( $key, $value, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
68
69        $this->wasLastGetCached = false;
70        return $value;
71    }
72
73    /** @inheritDoc */
74    public function getMulti( array $keys, $flags = 0 ) {
75        $this->wasLastGetCached = null;
76        $valueByKeyCached = [];
77
78        $keysFetch = [];
79        foreach ( $keys as $key ) {
80            $value = $this->procCache->get( $key, $flags );
81            if ( $value === false && !$this->procCache->hasKey( $key ) ) {
82                $keysFetch[] = $key;
83            } else {
84                $valueByKeyCached[$key] = $value;
85            }
86        }
87
88        $valueByKeyFetched = $this->store->proxyCall(
89            __FUNCTION__,
90            self::ARG0_KEYARR,
91            self::RES_KEYMAP,
92            [ $keysFetch, $flags ],
93            $this
94        );
95        $this->setMulti( $valueByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY );
96
97        return $valueByKeyCached + $valueByKeyFetched;
98    }
99
100    /** @inheritDoc */
101    public function set( $key, $value, $exptime = 0, $flags = 0 ) {
102        $this->procCache->set( $key, $value, $exptime, $flags );
103
104        if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
105            return true;
106        }
107
108        return $this->store->proxyCall(
109            __FUNCTION__,
110            self::ARG0_KEY,
111            self::RES_NONKEY,
112            func_get_args(),
113            $this
114        );
115    }
116
117    /** @inheritDoc */
118    public function delete( $key, $flags = 0 ) {
119        $this->procCache->delete( $key, $flags );
120
121        if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
122            return true;
123        }
124
125        return $this->store->proxyCall(
126            __FUNCTION__,
127            self::ARG0_KEY,
128            self::RES_NONKEY,
129            func_get_args(),
130            $this
131        );
132    }
133
134    /** @inheritDoc */
135    public function add( $key, $value, $exptime = 0, $flags = 0 ) {
136        if ( $this->get( $key ) === false ) {
137            return $this->set( $key, $value, $exptime, $flags );
138        }
139
140        // key already set
141        return false;
142    }
143
144    // These just call the backend (tested elsewhere)
145    // @codeCoverageIgnoreStart
146
147    /** @inheritDoc */
148    public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
149        $this->procCache->delete( $key );
150
151        return $this->store->proxyCall(
152            __FUNCTION__,
153            self::ARG0_KEY,
154            self::RES_NONKEY,
155            func_get_args(),
156            $this
157        );
158    }
159
160    /** @inheritDoc */
161    public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
162        $this->procCache->delete( $key );
163
164        return $this->store->proxyCall(
165            __FUNCTION__,
166            self::ARG0_KEY,
167            self::RES_NONKEY,
168            func_get_args(),
169            $this
170        );
171    }
172
173    /** @inheritDoc */
174    public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
175        return $this->store->proxyCall(
176            __FUNCTION__,
177            self::ARG0_KEY,
178            self::RES_NONKEY,
179            func_get_args(),
180            $this
181        );
182    }
183
184    /** @inheritDoc */
185    public function unlock( $key ) {
186        return $this->store->proxyCall(
187            __FUNCTION__,
188            self::ARG0_KEY,
189            self::RES_NONKEY,
190            func_get_args(),
191            $this
192        );
193    }
194
195    /** @inheritDoc */
196    public function deleteObjectsExpiringBefore(
197        $timestamp,
198        ?callable $progress = null,
199        $limit = INF,
200        ?string $tag = null
201    ) {
202        $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag );
203
204        return $this->store->proxyCall(
205            __FUNCTION__,
206            self::ARG0_NONKEY,
207            self::RES_NONKEY,
208            func_get_args(),
209            $this
210        );
211    }
212
213    /** @inheritDoc */
214    public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
215        $this->procCache->setMulti( $valueByKey, $exptime, $flags );
216
217        if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
218            return true;
219        }
220
221        return $this->store->proxyCall(
222            __FUNCTION__,
223            self::ARG0_KEYMAP,
224            self::RES_NONKEY,
225            func_get_args(),
226            $this
227        );
228    }
229
230    /** @inheritDoc */
231    public function deleteMulti( array $keys, $flags = 0 ) {
232        $this->procCache->deleteMulti( $keys, $flags );
233
234        if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
235            return true;
236        }
237
238        return $this->store->proxyCall(
239            __FUNCTION__,
240            self::ARG0_KEYARR,
241            self::RES_NONKEY,
242            func_get_args(),
243            $this
244        );
245    }
246
247    /** @inheritDoc */
248    public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
249        $this->procCache->changeTTLMulti( $keys, $exptime, $flags );
250
251        if ( $this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
252            return true;
253        }
254
255        return $this->store->proxyCall(
256            __FUNCTION__,
257            self::ARG0_KEYARR,
258            self::RES_NONKEY,
259            func_get_args(),
260            $this
261        );
262    }
263
264    /** @inheritDoc */
265    public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
266        $this->procCache->delete( $key );
267
268        return $this->store->proxyCall(
269            __FUNCTION__,
270            self::ARG0_KEY,
271            self::RES_NONKEY,
272            func_get_args(),
273            $this
274        );
275    }
276
277    /** @inheritDoc */
278    public function setMockTime( &$time ) {
279        parent::setMockTime( $time );
280        $this->procCache->setMockTime( $time );
281        $this->store->setMockTime( $time );
282    }
283
284    /**
285     * True if the last get() call was read from cache, false if it was a cache miss.
286     * Should be called immediately after a get() call (might throw otherwise).
287     * getMulti() is not supported.
288     * @since 1.45
289     */
290    public function wasLastGetCached(): bool {
291        if ( $this->wasLastGetCached === null ) {
292            throw new LogicException( __METHOD__ . ' must be called immediately after get()' );
293        }
294
295        $status = $this->wasLastGetCached;
296        // Disallow multiple calls without a get() in between as we expect this method to
297        // be used right after a get().
298        $this->wasLastGetCached = null;
299
300        return $status;
301    }
302
303    // @codeCoverageIgnoreEnd
304}
305
306/** @deprecated class alias since 1.43 */
307class_alias( CachedBagOStuff::class, 'CachedBagOStuff' );