Merge "Add .pipeline/ with dev image variant"
[lhc/web/wiklou.git] / includes / libs / objectcache / MultiWriteBagOStuff.php
1 <?php
2 /**
3 * Wrapper for object caching in different caches.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Cache
22 */
23 use Wikimedia\ObjectFactory;
24
25 /**
26 * A cache class that replicates all writes to multiple child caches. Reads
27 * are implemented by reading from the caches in the order they are given in
28 * the configuration until a cache gives a positive result.
29 *
30 * Note that cache key construction will use the first cache backend in the list,
31 * so make sure that the other backends can handle such keys (e.g. via encoding).
32 *
33 * @ingroup Cache
34 */
35 class MultiWriteBagOStuff extends BagOStuff {
36 /** @var BagOStuff[] */
37 protected $caches;
38 /** @var bool Use async secondary writes */
39 protected $asyncWrites = false;
40 /** @var int[] List of all backing cache indexes */
41 protected $cacheIndexes = [];
42
43 /** @var int TTL when a key is copied to a higher cache tier */
44 private static $UPGRADE_TTL = 3600;
45
46 /**
47 * $params include:
48 * - caches: A numbered array of either ObjectFactory::getObjectFromSpec
49 * arrays yielding BagOStuff objects or direct BagOStuff objects.
50 * If using the former, the 'args' field *must* be set.
51 * The first cache is the primary one, being the first to
52 * be read in the fallback chain. Writes happen to all stores
53 * in the order they are defined. However, lock()/unlock() calls
54 * only use the primary store.
55 * - replication: Either 'sync' or 'async'. This controls whether writes
56 * to secondary stores are deferred when possible. Async writes
57 * require setting 'asyncHandler'. HHVM register_postsend_function() function.
58 * Async writes can increase the chance of some race conditions
59 * or cause keys to expire seconds later than expected. It is
60 * safe to use for modules when cached values: are immutable,
61 * invalidation uses logical TTLs, invalidation uses etag/timestamp
62 * validation against the DB, or merge() is used to handle races.
63 * @param array $params
64 * @phan-param array{caches:array<int,array|BagOStuff>,replication:string} $params
65 * @throws InvalidArgumentException
66 */
67 public function __construct( $params ) {
68 parent::__construct( $params );
69
70 if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
71 throw new InvalidArgumentException(
72 __METHOD__ . ': "caches" parameter must be an array of caches'
73 );
74 }
75
76 $this->caches = [];
77 foreach ( $params['caches'] as $cacheInfo ) {
78 if ( $cacheInfo instanceof BagOStuff ) {
79 $this->caches[] = $cacheInfo;
80 } else {
81 if ( !isset( $cacheInfo['args'] ) ) {
82 // B/C for when $cacheInfo was for ObjectCache::newFromParams().
83 // Callers intenting this to be for ObjectFactory::getObjectFromSpec
84 // should have set "args" per the docs above. Doings so avoids extra
85 // (likely harmless) params (factory/class/calls) ending up in "args".
86 $cacheInfo['args'] = [ $cacheInfo ];
87 }
88 $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
89 }
90 }
91 $this->mergeFlagMaps( $this->caches );
92
93 $this->asyncWrites = (
94 isset( $params['replication'] ) &&
95 $params['replication'] === 'async' &&
96 is_callable( $this->asyncHandler )
97 );
98
99 $this->cacheIndexes = array_keys( $this->caches );
100 }
101
102 public function setDebug( $enabled ) {
103 parent::setDebug( $enabled );
104 foreach ( $this->caches as $cache ) {
105 $cache->setDebug( $enabled );
106 }
107 }
108
109 public function get( $key, $flags = 0 ) {
110 if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
111 // If the latest write was a delete(), we do NOT want to fallback
112 // to the other tiers and possibly see the old value. Also, this
113 // is used by merge(), which only needs to hit the primary.
114 return $this->caches[0]->get( $key, $flags );
115 }
116
117 $value = false;
118 $missIndexes = []; // backends checked
119 foreach ( $this->caches as $i => $cache ) {
120 $value = $cache->get( $key, $flags );
121 if ( $value !== false ) {
122 break;
123 }
124 $missIndexes[] = $i;
125 }
126
127 if (
128 $value !== false &&
129 $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
130 $missIndexes
131 ) {
132 // Backfill the value to the higher (and often faster/smaller) cache tiers
133 $this->doWrite(
134 $missIndexes,
135 $this->asyncWrites,
136 'set',
137 // @TODO: consider using self::WRITE_ALLOW_SEGMENTS here?
138 [ $key, $value, self::$UPGRADE_TTL ]
139 );
140 }
141
142 return $value;
143 }
144
145 public function set( $key, $value, $exptime = 0, $flags = 0 ) {
146 return $this->doWrite(
147 $this->cacheIndexes,
148 $this->usesAsyncWritesGivenFlags( $flags ),
149 __FUNCTION__,
150 func_get_args()
151 );
152 }
153
154 public function delete( $key, $flags = 0 ) {
155 return $this->doWrite(
156 $this->cacheIndexes,
157 $this->usesAsyncWritesGivenFlags( $flags ),
158 __FUNCTION__,
159 func_get_args()
160 );
161 }
162
163 public function add( $key, $value, $exptime = 0, $flags = 0 ) {
164 // Try the write to the top-tier cache
165 $ok = $this->doWrite(
166 [ 0 ],
167 $this->usesAsyncWritesGivenFlags( $flags ),
168 __FUNCTION__,
169 func_get_args()
170 );
171
172 if ( $ok ) {
173 // Relay the add() using set() if it succeeded. This is meant to handle certain
174 // migration scenarios where the same store might get written to twice for certain
175 // keys. In that case, it does not make sense to return false due to "self-conflicts".
176 return $this->doWrite(
177 array_slice( $this->cacheIndexes, 1 ),
178 $this->usesAsyncWritesGivenFlags( $flags ),
179 'set',
180 [ $key, $value, $exptime, $flags ]
181 );
182 }
183
184 return false;
185 }
186
187 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
188 return $this->doWrite(
189 $this->cacheIndexes,
190 $this->usesAsyncWritesGivenFlags( $flags ),
191 __FUNCTION__,
192 func_get_args()
193 );
194 }
195
196 public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
197 return $this->doWrite(
198 $this->cacheIndexes,
199 $this->usesAsyncWritesGivenFlags( $flags ),
200 __FUNCTION__,
201 func_get_args()
202 );
203 }
204
205 public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
206 // Only need to lock the first cache; also avoids deadlocks
207 return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
208 }
209
210 public function unlock( $key ) {
211 // Only the first cache is locked
212 return $this->caches[0]->unlock( $key );
213 }
214
215 public function deleteObjectsExpiringBefore(
216 $timestamp,
217 callable $progress = null,
218 $limit = INF
219 ) {
220 $ret = false;
221 foreach ( $this->caches as $cache ) {
222 if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
223 $ret = true;
224 }
225 }
226
227 return $ret;
228 }
229
230 public function getMulti( array $keys, $flags = 0 ) {
231 // Just iterate over each key in order to handle all the backfill logic
232 $res = [];
233 foreach ( $keys as $key ) {
234 $val = $this->get( $key, $flags );
235 if ( $val !== false ) {
236 $res[$key] = $val;
237 }
238 }
239
240 return $res;
241 }
242
243 public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
244 return $this->doWrite(
245 $this->cacheIndexes,
246 $this->usesAsyncWritesGivenFlags( $flags ),
247 __FUNCTION__,
248 func_get_args()
249 );
250 }
251
252 public function deleteMulti( array $data, $flags = 0 ) {
253 return $this->doWrite(
254 $this->cacheIndexes,
255 $this->usesAsyncWritesGivenFlags( $flags ),
256 __FUNCTION__,
257 func_get_args()
258 );
259 }
260
261 public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
262 return $this->doWrite(
263 $this->cacheIndexes,
264 $this->usesAsyncWritesGivenFlags( $flags ),
265 __FUNCTION__,
266 func_get_args()
267 );
268 }
269
270 public function incr( $key, $value = 1, $flags = 0 ) {
271 return $this->doWrite(
272 $this->cacheIndexes,
273 $this->asyncWrites,
274 __FUNCTION__,
275 func_get_args()
276 );
277 }
278
279 public function decr( $key, $value = 1, $flags = 0 ) {
280 return $this->doWrite(
281 $this->cacheIndexes,
282 $this->asyncWrites,
283 __FUNCTION__,
284 func_get_args()
285 );
286 }
287
288 public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
289 return $this->doWrite(
290 $this->cacheIndexes,
291 $this->asyncWrites,
292 __FUNCTION__,
293 func_get_args()
294 );
295 }
296
297 public function getLastError() {
298 return $this->caches[0]->getLastError();
299 }
300
301 public function clearLastError() {
302 $this->caches[0]->clearLastError();
303 }
304
305 /**
306 * Apply a write method to the backing caches specified by $indexes (in order)
307 *
308 * @param int[] $indexes List of backing cache indexes
309 * @param bool $asyncWrites
310 * @param string $method Method name of backing caches
311 * @param array $args Arguments to the method of backing caches
312 * @return bool
313 */
314 protected function doWrite( $indexes, $asyncWrites, $method, array $args ) {
315 $ret = true;
316
317 if ( array_diff( $indexes, [ 0 ] ) && $asyncWrites && $method !== 'merge' ) {
318 // Deep-clone $args to prevent misbehavior when something writes an
319 // object to the BagOStuff then modifies it afterwards, e.g. T168040.
320 $args = unserialize( serialize( $args ) );
321 }
322
323 foreach ( $indexes as $i ) {
324 $cache = $this->caches[$i];
325 if ( $i == 0 || !$asyncWrites ) {
326 // First store or in sync mode: write now and get result
327 if ( !$cache->$method( ...$args ) ) {
328 $ret = false;
329 }
330 } else {
331 // Secondary write in async mode: do not block this HTTP request
332 $logger = $this->logger;
333 ( $this->asyncHandler )(
334 function () use ( $cache, $method, $args, $logger ) {
335 if ( !$cache->$method( ...$args ) ) {
336 $logger->warning( "Async $method op failed" );
337 }
338 }
339 );
340 }
341 }
342
343 return $ret;
344 }
345
346 /**
347 * @param int $flags
348 * @return bool
349 */
350 protected function usesAsyncWritesGivenFlags( $flags ) {
351 return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
352 }
353
354 public function makeKeyInternal( $keyspace, $args ) {
355 return $this->caches[0]->makeKeyInternal( $keyspace, $args );
356 }
357
358 public function makeKey( $class, ...$components ) {
359 return $this->caches[0]->makeKey( ...func_get_args() );
360 }
361
362 public function makeGlobalKey( $class, ...$components ) {
363 return $this->caches[0]->makeGlobalKey( ...func_get_args() );
364 }
365
366 public function addBusyCallback( callable $workCallback ) {
367 $this->caches[0]->addBusyCallback( $workCallback );
368 }
369
370 public function setMockTime( &$time ) {
371 parent::setMockTime( $time );
372 foreach ( $this->caches as $cache ) {
373 $cache->setMockTime( $time );
374 $cache->setMockTime( $time );
375 }
376 }
377 }