MediaWiki master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1<?php
7
8use InvalidArgumentException;
9use Wikimedia\ObjectFactory\ObjectFactory;
10
91 protected $caches;
92
94 protected $asyncWrites = false;
96 protected $cacheIndexes = [];
97
99 private static $UPGRADE_TTL = 3600;
100
123 public function __construct( $params ) {
124 parent::__construct( $params );
125
126 if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
127 throw new InvalidArgumentException(
128 __METHOD__ . ': "caches" parameter must be an array of caches'
129 );
130 }
131
132 $this->caches = [];
133 foreach ( $params['caches'] as $cacheInfo ) {
134 if ( $cacheInfo instanceof BagOStuff ) {
135 $this->caches[] = $cacheInfo;
136 } else {
137 $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
138 }
139 }
140
141 $this->attrMap = $this->mergeFlagMaps( $this->caches );
142
143 $this->asyncWrites = (
144 isset( $params['replication'] ) &&
145 $params['replication'] === 'async' &&
146 is_callable( $this->asyncHandler )
147 );
148
149 $this->cacheIndexes = array_keys( $this->caches );
150 }
151
153 public function get( $key, $flags = 0 ) {
154 $args = func_get_args();
155
156 if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
157 // If the latest write was a delete(), we do NOT want to fallback
158 // to the other tiers and possibly see the old value. Also, this
159 // is used by merge(), which only needs to hit the primary.
160 return $this->callKeyMethodOnTierCache(
161 0,
162 __FUNCTION__,
163 self::ARG0_KEY,
164 self::RES_NONKEY,
165 $args
166 );
167 }
168
169 $value = false;
170 // backends checked
171 $missIndexes = [];
172 foreach ( $this->cacheIndexes as $i ) {
173 $value = $this->callKeyMethodOnTierCache(
174 $i,
175 __FUNCTION__,
176 self::ARG0_KEY,
177 self::RES_NONKEY,
178 $args
179 );
180 if ( $value !== false ) {
181 break;
182 }
183 $missIndexes[] = $i;
184 }
185
186 if (
187 $value !== false &&
188 $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
189 $missIndexes
190 ) {
191 // Backfill the value to the higher (and often faster/smaller) cache tiers
192 $this->callKeyWriteMethodOnTierCaches(
193 $missIndexes,
194 'set',
195 self::ARG0_KEY,
196 self::RES_NONKEY,
197 [ $key, $value, self::$UPGRADE_TTL ]
198 );
199 }
200
201 return $value;
202 }
203
205 public function set( $key, $value, $exptime = 0, $flags = 0 ) {
206 return $this->callKeyWriteMethodOnTierCaches(
207 $this->cacheIndexes,
208 __FUNCTION__,
209 self::ARG0_KEY,
210 self::RES_NONKEY,
211 func_get_args()
212 );
213 }
214
216 public function delete( $key, $flags = 0 ) {
217 return $this->callKeyWriteMethodOnTierCaches(
218 $this->cacheIndexes,
219 __FUNCTION__,
220 self::ARG0_KEY,
221 self::RES_NONKEY,
222 func_get_args()
223 );
224 }
225
227 public function add( $key, $value, $exptime = 0, $flags = 0 ) {
228 // Try the write to the top-tier cache
229 $ok = $this->callKeyMethodOnTierCache(
230 0,
231 __FUNCTION__,
232 self::ARG0_KEY,
233 self::RES_NONKEY,
234 func_get_args()
235 );
236
237 if ( $ok ) {
238 // Relay the add() using set() if it succeeded. This is meant to handle certain
239 // migration scenarios where the same store might get written to twice for certain
240 // keys. In that case, it makes no sense to return false due to "self-conflicts".
241 $okSecondaries = $this->callKeyWriteMethodOnTierCaches(
242 array_slice( $this->cacheIndexes, 1 ),
243 'set',
244 self::ARG0_KEY,
245 self::RES_NONKEY,
246 [ $key, $value, $exptime, $flags ]
247 );
248 if ( $okSecondaries === false ) {
249 $ok = false;
250 }
251 }
252
253 return $ok;
254 }
255
257 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
258 return $this->callKeyWriteMethodOnTierCaches(
259 $this->cacheIndexes,
260 __FUNCTION__,
261 self::ARG0_KEY,
262 self::RES_NONKEY,
263 func_get_args()
264 );
265 }
266
268 public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
269 return $this->callKeyWriteMethodOnTierCaches(
270 $this->cacheIndexes,
271 __FUNCTION__,
272 self::ARG0_KEY,
273 self::RES_NONKEY,
274 func_get_args()
275 );
276 }
277
279 public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
280 // Only need to lock the first cache; also avoids deadlocks
281 return $this->callKeyMethodOnTierCache(
282 0,
283 __FUNCTION__,
284 self::ARG0_KEY,
285 self::RES_NONKEY,
286 func_get_args()
287 );
288 }
289
291 public function unlock( $key ) {
292 // Only the first cache is locked
293 return $this->callKeyMethodOnTierCache(
294 0,
295 __FUNCTION__,
296 self::ARG0_KEY,
297 self::RES_NONKEY,
298 func_get_args()
299 );
300 }
301
304 $timestamp,
305 ?callable $progress = null,
306 $limit = INF,
307 ?string $tag = null
308 ) {
309 $ret = false;
310 foreach ( $this->caches as $cache ) {
311 if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag ) ) {
312 $ret = true;
313 }
314 }
315
316 return $ret;
317 }
318
320 public function getMulti( array $keys, $flags = 0 ) {
321 // Just iterate over each key in order to handle all the backfill logic
322 $res = [];
323 foreach ( $keys as $key ) {
324 $val = $this->get( $key, $flags );
325 if ( $val !== false ) {
326 $res[$key] = $val;
327 }
328 }
329
330 return $res;
331 }
332
334 public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
335 return $this->callKeyWriteMethodOnTierCaches(
336 $this->cacheIndexes,
337 __FUNCTION__,
338 self::ARG0_KEYMAP,
339 self::RES_NONKEY,
340 func_get_args()
341 );
342 }
343
345 public function deleteMulti( array $keys, $flags = 0 ) {
346 return $this->callKeyWriteMethodOnTierCaches(
347 $this->cacheIndexes,
348 __FUNCTION__,
349 self::ARG0_KEYARR,
350 self::RES_NONKEY,
351 func_get_args()
352 );
353 }
354
356 public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
357 return $this->callKeyWriteMethodOnTierCaches(
358 $this->cacheIndexes,
359 __FUNCTION__,
360 self::ARG0_KEYARR,
361 self::RES_NONKEY,
362 func_get_args()
363 );
364 }
365
367 public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
368 return $this->callKeyWriteMethodOnTierCaches(
369 $this->cacheIndexes,
370 __FUNCTION__,
371 self::ARG0_KEY,
372 self::RES_NONKEY,
373 func_get_args()
374 );
375 }
376
378 public function setMockTime( &$time ) {
379 parent::setMockTime( $time );
380 foreach ( $this->caches as $cache ) {
381 $cache->setMockTime( $time );
382 }
383 }
384
396 private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
397 return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args, $this );
398 }
399
411 private function callKeyWriteMethodOnTierCaches(
412 array $indexes,
413 $method,
414 $arg0Sig,
415 $resSig,
416 array $args
417 ) {
418 $res = null;
419
420 if ( $this->asyncWrites && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
421 // Deep-clone $args to prevent misbehavior when something writes an
422 // object to the BagOStuff then modifies it afterwards, e.g. T168040.
423 $args = unserialize( serialize( $args ) );
424 }
425
426 foreach ( $indexes as $i ) {
427 $cache = $this->caches[$i];
428
429 if ( $i == 0 || !$this->asyncWrites ) {
430 // Tier 0 store or in sync mode: write synchronously and get result
431 $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
432 if ( $storeRes === false ) {
433 $res = false;
434 } elseif ( $res === null ) {
435 // first synchronous result
436 $res = $storeRes;
437 }
438 } else {
439 // Secondary write in async mode: do not block this HTTP request
441 function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
442 $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
443 }
444 );
445 }
446 }
447
448 return $res;
449 }
450}
451
453class_alias( MultiWriteBagOStuff::class, 'MultiWriteBagOStuff' );
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
Wrap multiple BagOStuff objects, to implement different caching tiers.
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple items.BagOStuff::changeTTL()bool Success (all items found and updat...
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.bool Success (item created)
deleteObjectsExpiringBefore( $timestamp, ?callable $progress=null, $limit=INF, ?string $tag=null)
Delete all objects expiring before a certain date.bool Success; false if unimplemented
int[] $cacheIndexes
List of all backing cache indexes.
deleteMulti(array $keys, $flags=0)
Delete a batch of items.This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/OWRITE_B...
setMulti(array $valueByKey, $exptime=0, $flags=0)
Set a batch of items.This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/OWRITE_BACK...
getMulti(array $keys, $flags=0)
Get a batch of items.mixed[] Map of (key => value) for existing keys
unlock( $key)
Release an advisory lock on a key string.bool Success
lock( $key, $timeout=6, $exptime=6, $rclass='')
Acquire an advisory lock on a key string, exclusive to the caller.bool Success
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on an item.If an expiry in the past is given then the key will immediately be e...
incrWithInit( $key, $exptime, $step=1, $init=null, $flags=0)
Increase the value of the given key (no TTL change) if it exists or create it otherwise....
BagOStuff[] $caches
Backing cache stores in order of highest to lowest tier.
bool $asyncWrites
Use async secondary writes.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)The callback function return...