MediaWiki  master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\ObjectFactory;
24 
38  protected $caches;
39 
41  protected $asyncWrites = false;
43  protected $cacheIndexes = [];
44 
46  private static $UPGRADE_TTL = 3600;
47 
69  public function __construct( $params ) {
70  parent::__construct( $params );
71 
72  if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
73  throw new InvalidArgumentException(
74  __METHOD__ . ': "caches" parameter must be an array of caches'
75  );
76  }
77 
78  $this->caches = [];
79  foreach ( $params['caches'] as $cacheInfo ) {
80  if ( $cacheInfo instanceof BagOStuff ) {
81  $this->caches[] = $cacheInfo;
82  } else {
83  if ( !isset( $cacheInfo['args'] ) ) {
84  // B/C for when $cacheInfo was for ObjectCache::newFromParams().
85  // Callers intenting this to be for ObjectFactory::getObjectFromSpec
86  // should have set "args" per the docs above. Doings so avoids extra
87  // (likely harmless) params (factory/class/calls) ending up in "args".
88  $cacheInfo['args'] = [ $cacheInfo ];
89  }
90 
91  // ObjectFactory::getObjectFromSpec accepts an array, not just a callable (phan bug)
92  // @phan-suppress-next-line PhanTypeInvalidCallableArraySize
93  $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
94  }
95  }
96  $this->mergeFlagMaps( $this->caches );
97 
98  $this->asyncWrites = (
99  isset( $params['replication'] ) &&
100  $params['replication'] === 'async' &&
101  is_callable( $this->asyncHandler )
102  );
103 
104  $this->cacheIndexes = array_keys( $this->caches );
105  }
106 
107  public function get( $key, $flags = 0 ) {
108  $args = func_get_args();
109 
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->callKeyMethodOnTierCache(
115  0,
116  __FUNCTION__,
117  self::ARG0_KEY,
118  self::RES_NONKEY,
119  $args
120  );
121  }
122 
123  $value = false;
124  $missIndexes = []; // backends checked
125  foreach ( $this->cacheIndexes as $i ) {
126  $value = $this->callKeyMethodOnTierCache(
127  $i,
128  __FUNCTION__,
129  self::ARG0_KEY,
130  self::RES_NONKEY,
131  $args
132  );
133  if ( $value !== false ) {
134  break;
135  }
136  $missIndexes[] = $i;
137  }
138 
139  if (
140  $value !== false &&
141  $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
142  $missIndexes
143  ) {
144  // Backfill the value to the higher (and often faster/smaller) cache tiers
146  $missIndexes,
147  $this->asyncWrites,
148  'set',
149  self::ARG0_KEY,
150  self::RES_NONKEY,
151  [ $key, $value, self::$UPGRADE_TTL ]
152  );
153  }
154 
155  return $value;
156  }
157 
158  public function set( $key, $value, $exptime = 0, $flags = 0 ) {
159  return $this->callKeyWriteMethodOnTierCaches(
160  $this->cacheIndexes,
161  $this->useAsyncSecondaryWrites( $flags ),
162  __FUNCTION__,
163  self::ARG0_KEY,
164  self::RES_NONKEY,
165  func_get_args()
166  );
167  }
168 
169  public function delete( $key, $flags = 0 ) {
170  return $this->callKeyWriteMethodOnTierCaches(
171  $this->cacheIndexes,
172  $this->useAsyncSecondaryWrites( $flags ),
173  __FUNCTION__,
174  self::ARG0_KEY,
175  self::RES_NONKEY,
176  func_get_args()
177  );
178  }
179 
180  public function add( $key, $value, $exptime = 0, $flags = 0 ) {
181  // Try the write to the top-tier cache
182  $ok = $this->callKeyMethodOnTierCache(
183  0,
184  __FUNCTION__,
185  self::ARG0_KEY,
186  self::RES_NONKEY,
187  func_get_args()
188  );
189 
190  if ( $ok ) {
191  // Relay the add() using set() if it succeeded. This is meant to handle certain
192  // migration scenarios where the same store might get written to twice for certain
193  // keys. In that case, it makes no sense to return false due to "self-conflicts".
194  $okSecondaries = $this->callKeyWriteMethodOnTierCaches(
195  array_slice( $this->cacheIndexes, 1 ),
196  $this->useAsyncSecondaryWrites( $flags ),
197  'set',
198  self::ARG0_KEY,
199  self::RES_NONKEY,
200  [ $key, $value, $exptime, $flags ]
201  );
202  if ( $okSecondaries === false ) {
203  $ok = false;
204  }
205  }
206 
207  return $ok;
208  }
209 
210  public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
211  return $this->callKeyWriteMethodOnTierCaches(
212  $this->cacheIndexes,
213  $this->useAsyncSecondaryWrites( $flags ),
214  __FUNCTION__,
215  self::ARG0_KEY,
216  self::RES_NONKEY,
217  func_get_args()
218  );
219  }
220 
221  public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
222  return $this->callKeyWriteMethodOnTierCaches(
223  $this->cacheIndexes,
224  $this->useAsyncSecondaryWrites( $flags ),
225  __FUNCTION__,
226  self::ARG0_KEY,
227  self::RES_NONKEY,
228  func_get_args()
229  );
230  }
231 
232  public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
233  // Only need to lock the first cache; also avoids deadlocks
234  return $this->callKeyMethodOnTierCache(
235  0,
236  __FUNCTION__,
237  self::ARG0_KEY,
238  self::RES_NONKEY,
239  func_get_args()
240  );
241  }
242 
243  public function unlock( $key ) {
244  // Only the first cache is locked
245  return $this->callKeyMethodOnTierCache(
246  0,
247  __FUNCTION__,
248  self::ARG0_KEY,
249  self::RES_NONKEY,
250  func_get_args()
251  );
252  }
253 
255  $timestamp,
256  callable $progress = null,
257  $limit = INF
258  ) {
259  $ret = false;
260  foreach ( $this->caches as $cache ) {
261  if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
262  $ret = true;
263  }
264  }
265 
266  return $ret;
267  }
268 
269  public function getMulti( array $keys, $flags = 0 ) {
270  // Just iterate over each key in order to handle all the backfill logic
271  $res = [];
272  foreach ( $keys as $key ) {
273  $val = $this->get( $key, $flags );
274  if ( $val !== false ) {
275  $res[$key] = $val;
276  }
277  }
278 
279  return $res;
280  }
281 
282  public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
283  return $this->callKeyWriteMethodOnTierCaches(
284  $this->cacheIndexes,
285  $this->useAsyncSecondaryWrites( $flags ),
286  __FUNCTION__,
287  self::ARG0_KEYMAP,
288  self::RES_NONKEY,
289  func_get_args()
290  );
291  }
292 
293  public function deleteMulti( array $keys, $flags = 0 ) {
294  return $this->callKeyWriteMethodOnTierCaches(
295  $this->cacheIndexes,
296  $this->useAsyncSecondaryWrites( $flags ),
297  __FUNCTION__,
298  self::ARG0_KEYARR,
299  self::RES_NONKEY,
300  func_get_args()
301  );
302  }
303 
304  public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
305  return $this->callKeyWriteMethodOnTierCaches(
306  $this->cacheIndexes,
307  $this->useAsyncSecondaryWrites( $flags ),
308  __FUNCTION__,
309  self::ARG0_KEYARR,
310  self::RES_NONKEY,
311  func_get_args()
312  );
313  }
314 
315  public function incr( $key, $value = 1, $flags = 0 ) {
316  return $this->callKeyWriteMethodOnTierCaches(
317  $this->cacheIndexes,
318  $this->useAsyncSecondaryWrites( $flags ),
319  __FUNCTION__,
320  self::ARG0_KEY,
321  self::RES_NONKEY,
322  func_get_args()
323  );
324  }
325 
326  public function decr( $key, $value = 1, $flags = 0 ) {
327  return $this->callKeyWriteMethodOnTierCaches(
328  $this->cacheIndexes,
329  $this->useAsyncSecondaryWrites( $flags ),
330  __FUNCTION__,
331  self::ARG0_KEY,
332  self::RES_NONKEY,
333  func_get_args()
334  );
335  }
336 
337  public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
338  return $this->callKeyWriteMethodOnTierCaches(
339  $this->cacheIndexes,
340  $this->useAsyncSecondaryWrites( $flags ),
341  __FUNCTION__,
342  self::ARG0_KEY,
343  self::RES_NONKEY,
344  func_get_args()
345  );
346  }
347 
348  public function getLastError() {
349  foreach ( $this->caches as $cache ) {
350  $error = $cache->getLastError();
351  if ( $error !== self::ERR_NONE ) {
352  return $error;
353  }
354  }
355 
356  return self::ERR_NONE;
357  }
358 
359  public function clearLastError() {
360  foreach ( $this->caches as $cache ) {
361  $cache->clearLastError();
362  }
363  }
364 
365  public function makeKeyInternal( $keyspace, $components ) {
366  return $this->genericKeyFromComponents( $keyspace, ...$components );
367  }
368 
369  public function makeKey( $collection, ...$components ) {
370  return $this->genericKeyFromComponents( $this->keyspace, $collection, ...$components );
371  }
372 
373  public function makeGlobalKey( $collection, ...$components ) {
374  return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $collection, ...$components );
375  }
376 
377  protected function convertGenericKey( $key ) {
378  return $key; // short-circuit; already uses "generic" keys
379  }
380 
381  public function addBusyCallback( callable $workCallback ) {
382  $this->caches[0]->addBusyCallback( $workCallback );
383  }
384 
385  public function setNewPreparedValues( array $valueByKey ) {
386  return $this->callKeyMethodOnTierCache(
387  0,
388  __FUNCTION__,
389  self::ARG0_KEYMAP,
390  self::RES_NONKEY,
391  func_get_args()
392  );
393  }
394 
395  public function setMockTime( &$time ) {
396  parent::setMockTime( $time );
397  foreach ( $this->caches as $cache ) {
398  $cache->setMockTime( $time );
399  }
400  }
401 
412  private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
413  return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args );
414  }
415 
428  array $indexes,
429  $asyncSecondary,
430  $method,
431  $arg0Sig,
432  $resSig,
433  array $args
434  ) {
435  $res = null;
436 
437  if ( $asyncSecondary && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
438  // Deep-clone $args to prevent misbehavior when something writes an
439  // object to the BagOStuff then modifies it afterwards, e.g. T168040.
441  }
442 
443  foreach ( $indexes as $i ) {
444  $cache = $this->caches[$i];
445 
446  if ( $i == 0 || !$asyncSecondary ) {
447  // Tier 0 store or in sync mode: write synchronously and get result
448  $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args );
449  if ( $storeRes === false ) {
450  $res = false;
451  } elseif ( $res === null ) {
452  $res = $storeRes; // first synchronous result
453  }
454  } else {
455  // Secondary write in async mode: do not block this HTTP request
457  static function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
458  $cache->proxyCall( $method, $arg0Sig, $resSig, $args );
459  }
460  );
461  }
462  }
463 
464  return $res;
465  }
466 
471  private function useAsyncSecondaryWrites( $flags ) {
472  return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
473  }
474 }
MultiWriteBagOStuff\$cacheIndexes
int[] $cacheIndexes
List of all backing cache indexes.
Definition: MultiWriteBagOStuff.php:43
MultiWriteBagOStuff\lock
lock( $key, $timeout=6, $expiry=6, $rclass='')
Acquire an advisory lock on a key string.
Definition: MultiWriteBagOStuff.php:232
MultiWriteBagOStuff\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: MultiWriteBagOStuff.php:348
MultiWriteBagOStuff\$asyncWrites
bool $asyncWrites
Use async secondary writes.
Definition: MultiWriteBagOStuff.php:41
MultiWriteBagOStuff\clearLastError
clearLastError()
Clear the "last error" registry.
Definition: MultiWriteBagOStuff.php:359
MultiWriteBagOStuff\merge
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
Definition: MultiWriteBagOStuff.php:210
MultiWriteBagOStuff\decr
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
Definition: MultiWriteBagOStuff.php:326
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
BagOStuff\genericKeyFromComponents
genericKeyFromComponents(... $components)
At a minimum, there must be a keyspace and collection name component.
Definition: BagOStuff.php:685
MultiWriteBagOStuff\changeTTL
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on a key if it exists.
Definition: MultiWriteBagOStuff.php:221
MultiWriteBagOStuff\incrWithInit
incrWithInit( $key, $exptime, $value=1, $init=null, $flags=0)
Increase the value of the given key (no TTL change) if it exists or create it otherwise.
Definition: MultiWriteBagOStuff.php:337
$res
$res
Definition: testCompression.php:57
serialize
serialize()
Definition: ApiMessageTrait.php:138
MultiWriteBagOStuff\setMulti
setMulti(array $valueByKey, $exptime=0, $flags=0)
Batch insertion/replace.
Definition: MultiWriteBagOStuff.php:282
MultiWriteBagOStuff\addBusyCallback
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
Definition: MultiWriteBagOStuff.php:381
MultiWriteBagOStuff\makeKey
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
Definition: MultiWriteBagOStuff.php:369
MultiWriteBagOStuff\changeTTLMulti
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple keys that exist.
Definition: MultiWriteBagOStuff.php:304
MultiWriteBagOStuff\unlock
unlock( $key)
Release an advisory lock on a key string.
Definition: MultiWriteBagOStuff.php:243
BagOStuff\$asyncHandler
callable null $asyncHandler
Definition: BagOStuff.php:92
MultiWriteBagOStuff\useAsyncSecondaryWrites
useAsyncSecondaryWrites( $flags)
Definition: MultiWriteBagOStuff.php:471
$args
if( $line===false) $args
Definition: mcc.php:124
MultiWriteBagOStuff\convertGenericKey
convertGenericKey( $key)
Convert a "generic" reversible cache key into one for this cache.
Definition: MultiWriteBagOStuff.php:377
MultiWriteBagOStuff\add
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
Definition: MultiWriteBagOStuff.php:180
MultiWriteBagOStuff\incr
incr( $key, $value=1, $flags=0)
Increase stored value of $key by $value while preserving its TTL.
Definition: MultiWriteBagOStuff.php:315
MultiWriteBagOStuff\setMockTime
setMockTime(&$time)
Definition: MultiWriteBagOStuff.php:395
MultiWriteBagOStuff\callKeyWriteMethodOnTierCaches
callKeyWriteMethodOnTierCaches(array $indexes, $asyncSecondary, $method, $arg0Sig, $resSig, array $args)
Call a write method on the cache instances, in order, for the given tiers (indexes)
Definition: MultiWriteBagOStuff.php:427
MultiWriteBagOStuff\__construct
__construct( $params)
Definition: MultiWriteBagOStuff.php:69
BagOStuff\mergeFlagMaps
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
Definition: BagOStuff.php:603
MultiWriteBagOStuff\makeGlobalKey
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
Definition: MultiWriteBagOStuff.php:373
MultiWriteBagOStuff
A cache class that replicates all writes to multiple child caches.
Definition: MultiWriteBagOStuff.php:36
MultiWriteBagOStuff\$caches
BagOStuff[] $caches
Backing cache stores in order of highest to lowest tier.
Definition: MultiWriteBagOStuff.php:38
MultiWriteBagOStuff\deleteObjectsExpiringBefore
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF)
Delete all objects expiring before a certain date.
Definition: MultiWriteBagOStuff.php:254
unserialize
unserialize( $serialized)
Definition: ApiMessageTrait.php:146
$cache
$cache
Definition: mcc.php:33
MultiWriteBagOStuff\callKeyMethodOnTierCache
callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args)
Call a method on the cache instance for the given cache tier (index)
Definition: MultiWriteBagOStuff.php:412
MultiWriteBagOStuff\makeKeyInternal
makeKeyInternal( $keyspace, $components)
Make a cache key for the given keyspace and components.
Definition: MultiWriteBagOStuff.php:365
BagOStuff\fieldHasFlags
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:593
$keys
$keys
Definition: testCompression.php:72
MultiWriteBagOStuff\setNewPreparedValues
setNewPreparedValues(array $valueByKey)
Make a "generic" reversible cache key from the given components.
Definition: MultiWriteBagOStuff.php:385
MultiWriteBagOStuff\deleteMulti
deleteMulti(array $keys, $flags=0)
Batch deletion.
Definition: MultiWriteBagOStuff.php:293
BagOStuff\$keyspace
string $keyspace
Default keyspace; used by makeKey()
Definition: BagOStuff.php:103
MultiWriteBagOStuff\getMulti
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
Definition: MultiWriteBagOStuff.php:269
MultiWriteBagOStuff\$UPGRADE_TTL
static int $UPGRADE_TTL
TTL when a key is copied to a higher cache tier.
Definition: MultiWriteBagOStuff.php:46