Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.81% covered (danger)
34.81%
55 / 158
5.56% covered (danger)
5.56%
1 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReplicatedBagOStuff
34.81% covered (danger)
34.81%
55 / 158
5.56% covered (danger)
5.56%
1 / 18
297.23
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
9 / 15
0.00% covered (danger)
0.00%
0 / 1
6.60
 get
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 set
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 delete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 add
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 merge
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 changeTTL
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 lock
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 unlock
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 deleteObjectsExpiringBefore
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getMulti
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 setMulti
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 deleteMulti
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 changeTTLMulti
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 incrWithInit
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 setMockTime
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 hadRecentSessionWrite
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 remarkRecentSessionWrite
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
4.46
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 * @ingroup Cache
20 */
21
22use Wikimedia\ObjectCache\BagOStuff;
23use Wikimedia\ObjectFactory\ObjectFactory;
24
25/**
26 * A cache class that directs writes to one set of servers and reads to
27 * another. This assumes that the servers used for reads are setup to replica DB
28 * those that writes go to. This can easily be used with redis for example.
29 *
30 * In the WAN scenario (e.g. multi-datacenter case), this is useful when
31 * writes are rare or they usually take place in the primary datacenter.
32 *
33 * @deprecated since 1.42
34 * @ingroup Cache
35 * @since 1.26
36 */
37class ReplicatedBagOStuff extends BagOStuff {
38    /** @var BagOStuff */
39    private $writeStore;
40    /** @var BagOStuff */
41    private $readStore;
42
43    /** @var int Seconds to read from the master source for a key after writing to it */
44    private $consistencyWindow;
45    /** @var float[] Map of (key => UNIX timestamp) */
46    private $lastKeyWrites = [];
47
48    /** @var int Max expected delay (in seconds) for writes to reach replicas */
49    private const MAX_WRITE_DELAY = 5;
50
51    /**
52     * Constructor. Parameters are:
53     *   - writeFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff.
54     *      This object will be used for writes (e.g. the primary DB).
55     *   - readFactory: ObjectFactory::getObjectFromSpec array yielding BagOStuff.
56     *      This object will be used for reads (e.g. a replica DB).
57     *   - sessionConsistencyWindow: Seconds to read from the master source for a key
58     *      after writing to it. [Default: ReplicatedBagOStuff::MAX_WRITE_DELAY]
59     *
60     * @deprecated since 1.42
61     * @param array $params
62     * @throws InvalidArgumentException
63     */
64    public function __construct( $params ) {
65        parent::__construct( $params );
66
67        if ( !isset( $params['writeFactory'] ) ) {
68            throw new InvalidArgumentException(
69                __METHOD__ . ': the "writeFactory" parameter is required' );
70        } elseif ( !isset( $params['readFactory'] ) ) {
71            throw new InvalidArgumentException(
72                __METHOD__ . ': the "readFactory" parameter is required' );
73        }
74
75        $this->consistencyWindow = $params['sessionConsistencyWindow'] ?? self::MAX_WRITE_DELAY;
76        $this->writeStore = ( $params['writeFactory'] instanceof BagOStuff )
77            ? $params['writeFactory']
78            : ObjectFactory::getObjectFromSpec( $params['writeFactory'] );
79        $this->readStore = ( $params['readFactory'] instanceof BagOStuff )
80            ? $params['readFactory']
81            : ObjectFactory::getObjectFromSpec( $params['readFactory'] );
82        $this->attrMap = $this->mergeFlagMaps( [ $this->readStore, $this->writeStore ] );
83    }
84
85    public function get( $key, $flags = 0 ) {
86        $store = (
87            $this->hadRecentSessionWrite( [ $key ] ) ||
88            $this->fieldHasFlags( $flags, self::READ_LATEST )
89        )
90            // Try to maintain session consistency and respect READ_LATEST
91            ? $this->writeStore
92            // Otherwise, just use the default "read" store
93            : $this->readStore;
94
95        return $store->proxyCall(
96            __FUNCTION__,
97            self::ARG0_KEY,
98            self::RES_NONKEY,
99            func_get_args(),
100            $this
101        );
102    }
103
104    public function set( $key, $value, $exptime = 0, $flags = 0 ) {
105        $this->remarkRecentSessionWrite( [ $key ] );
106
107        return $this->writeStore->proxyCall(
108            __FUNCTION__,
109            self::ARG0_KEY,
110            self::RES_NONKEY,
111            func_get_args(),
112            $this
113        );
114    }
115
116    public function delete( $key, $flags = 0 ) {
117        $this->remarkRecentSessionWrite( [ $key ] );
118
119        return $this->writeStore->proxyCall(
120            __FUNCTION__,
121            self::ARG0_KEY,
122            self::RES_NONKEY,
123            func_get_args(),
124            $this
125        );
126    }
127
128    public function add( $key, $value, $exptime = 0, $flags = 0 ) {
129        $this->remarkRecentSessionWrite( [ $key ] );
130
131        return $this->writeStore->proxyCall(
132            __FUNCTION__,
133            self::ARG0_KEY,
134            self::RES_NONKEY,
135            func_get_args(),
136            $this
137        );
138    }
139
140    public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
141        $this->remarkRecentSessionWrite( [ $key ] );
142
143        return $this->writeStore->proxyCall(
144            __FUNCTION__,
145            self::ARG0_KEY,
146            self::RES_NONKEY,
147            func_get_args(),
148            $this
149        );
150    }
151
152    public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
153        $this->remarkRecentSessionWrite( [ $key ] );
154
155        return $this->writeStore->proxyCall(
156            __FUNCTION__,
157            self::ARG0_KEY,
158            self::RES_NONKEY,
159            func_get_args(),
160            $this
161        );
162    }
163
164    public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
165        return $this->writeStore->proxyCall(
166            __FUNCTION__,
167            self::ARG0_KEY,
168            self::RES_NONKEY,
169            func_get_args(),
170            $this
171        );
172    }
173
174    public function unlock( $key ) {
175        return $this->writeStore->proxyCall(
176            __FUNCTION__,
177            self::ARG0_KEY,
178            self::RES_NONKEY,
179            func_get_args(),
180            $this
181        );
182    }
183
184    public function deleteObjectsExpiringBefore(
185        $timestamp,
186        callable $progress = null,
187        $limit = INF,
188        string $tag = null
189    ) {
190        return $this->writeStore->proxyCall(
191            __FUNCTION__,
192            self::ARG0_NONKEY,
193            self::RES_NONKEY,
194            func_get_args(),
195            $this
196        );
197    }
198
199    public function getMulti( array $keys, $flags = 0 ) {
200        $store = (
201            $this->hadRecentSessionWrite( $keys ) ||
202            $this->fieldHasFlags( $flags, self::READ_LATEST )
203        )
204            // Try to maintain session consistency and respect READ_LATEST
205            ? $this->writeStore
206            // Otherwise, just use the default "read" store
207            : $this->readStore;
208
209        return $store->proxyCall(
210            __FUNCTION__,
211            self::ARG0_KEYARR,
212            self::RES_KEYMAP,
213            func_get_args(),
214            $this
215        );
216    }
217
218    public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
219        $this->remarkRecentSessionWrite( array_keys( $valueByKey ) );
220
221        return $this->writeStore->proxyCall(
222            __FUNCTION__,
223            self::ARG0_KEYMAP,
224            self::RES_NONKEY,
225            func_get_args(),
226            $this
227        );
228    }
229
230    public function deleteMulti( array $keys, $flags = 0 ) {
231        $this->remarkRecentSessionWrite( $keys );
232
233        return $this->writeStore->proxyCall(
234            __FUNCTION__,
235            self::ARG0_KEYARR,
236            self::RES_NONKEY,
237            func_get_args(),
238            $this
239        );
240    }
241
242    public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
243        $this->remarkRecentSessionWrite( $keys );
244
245        return $this->writeStore->proxyCall(
246            __FUNCTION__,
247            self::ARG0_KEYARR,
248            self::RES_NONKEY,
249            func_get_args(),
250            $this
251        );
252    }
253
254    public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
255        $this->remarkRecentSessionWrite( [ $key ] );
256
257        return $this->writeStore->proxyCall(
258            __FUNCTION__,
259            self::ARG0_KEY,
260            self::RES_NONKEY,
261            func_get_args(),
262            $this
263        );
264    }
265
266    public function setMockTime( &$time ) {
267        parent::setMockTime( $time );
268        $this->writeStore->setMockTime( $time );
269        $this->readStore->setMockTime( $time );
270    }
271
272    /**
273     * @param string[] $keys
274     * @return bool
275     */
276    private function hadRecentSessionWrite( array $keys ) {
277        $now = $this->getCurrentTime();
278        foreach ( $keys as $key ) {
279            $ts = $this->lastKeyWrites[$key] ?? 0;
280            if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) {
281                return true;
282            }
283        }
284
285        return false;
286    }
287
288    /**
289     * @param string[] $keys
290     */
291    private function remarkRecentSessionWrite( array $keys ) {
292        $now = $this->getCurrentTime();
293        foreach ( $keys as $key ) {
294            // move to the end
295            unset( $this->lastKeyWrites[$key] );
296            $this->lastKeyWrites[$key] = $now;
297        }
298        // Prune out the map if the first key is obsolete
299        if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) {
300            $this->lastKeyWrites = array_filter(
301                $this->lastKeyWrites,
302                function ( $timestamp ) use ( $now ) {
303                    return ( ( $now - $timestamp ) <= $this->consistencyWindow );
304                }
305            );
306        }
307    }
308}