MediaWiki  master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\ObjectFactory;
24 
38  protected $caches;
40  protected $asyncWrites = false;
42  protected $cacheIndexes = [];
43 
45  private static $UPGRADE_TTL = 3600;
46 
68  public function __construct( $params ) {
69  parent::__construct( $params );
70 
71  if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
72  throw new InvalidArgumentException(
73  __METHOD__ . ': "caches" parameter must be an array of caches'
74  );
75  }
76 
77  $this->caches = [];
78  foreach ( $params['caches'] as $cacheInfo ) {
79  if ( $cacheInfo instanceof BagOStuff ) {
80  $this->caches[] = $cacheInfo;
81  } else {
82  if ( !isset( $cacheInfo['args'] ) ) {
83  // B/C for when $cacheInfo was for ObjectCache::newFromParams().
84  // Callers intenting this to be for ObjectFactory::getObjectFromSpec
85  // should have set "args" per the docs above. Doings so avoids extra
86  // (likely harmless) params (factory/class/calls) ending up in "args".
87  $cacheInfo['args'] = [ $cacheInfo ];
88  }
89 
90  // ObjectFactory::getObjectFromSpec accepts an array, not just a callable (phan bug)
91  // @phan-suppress-next-line PhanTypeInvalidCallableArraySize
92  $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
93  }
94  }
95  $this->mergeFlagMaps( $this->caches );
96 
97  $this->asyncWrites = (
98  isset( $params['replication'] ) &&
99  $params['replication'] === 'async' &&
100  is_callable( $this->asyncHandler )
101  );
102 
103  $this->cacheIndexes = array_keys( $this->caches );
104  }
105 
106  public function setDebug( $enabled ) {
107  parent::setDebug( $enabled );
108  foreach ( $this->caches as $cache ) {
109  $cache->setDebug( $enabled );
110  }
111  }
112 
113  public function get( $key, $flags = 0 ) {
114  if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
115  // If the latest write was a delete(), we do NOT want to fallback
116  // to the other tiers and possibly see the old value. Also, this
117  // is used by merge(), which only needs to hit the primary.
118  return $this->caches[0]->get( $key, $flags );
119  }
120 
121  $value = false;
122  $missIndexes = []; // backends checked
123  foreach ( $this->caches as $i => $cache ) {
124  $value = $cache->get( $key, $flags );
125  if ( $value !== false ) {
126  break;
127  }
128  $missIndexes[] = $i;
129  }
130 
131  if (
132  $value !== false &&
133  $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
134  $missIndexes
135  ) {
136  // Backfill the value to the higher (and often faster/smaller) cache tiers
137  $this->doWrite(
138  $missIndexes,
139  $this->asyncWrites,
140  'set',
141  // @TODO: consider using self::WRITE_ALLOW_SEGMENTS here?
142  [ $key, $value, self::$UPGRADE_TTL ]
143  );
144  }
145 
146  return $value;
147  }
148 
149  public function set( $key, $value, $exptime = 0, $flags = 0 ) {
150  return $this->doWrite(
151  $this->cacheIndexes,
152  $this->usesAsyncWritesGivenFlags( $flags ),
153  __FUNCTION__,
154  func_get_args()
155  );
156  }
157 
158  public function delete( $key, $flags = 0 ) {
159  return $this->doWrite(
160  $this->cacheIndexes,
161  $this->usesAsyncWritesGivenFlags( $flags ),
162  __FUNCTION__,
163  func_get_args()
164  );
165  }
166 
167  public function add( $key, $value, $exptime = 0, $flags = 0 ) {
168  // Try the write to the top-tier cache
169  $ok = $this->doWrite(
170  [ 0 ],
171  $this->usesAsyncWritesGivenFlags( $flags ),
172  __FUNCTION__,
173  func_get_args()
174  );
175 
176  if ( $ok ) {
177  // Relay the add() using set() if it succeeded. This is meant to handle certain
178  // migration scenarios where the same store might get written to twice for certain
179  // keys. In that case, it does not make sense to return false due to "self-conflicts".
180  return $this->doWrite(
181  array_slice( $this->cacheIndexes, 1 ),
182  $this->usesAsyncWritesGivenFlags( $flags ),
183  'set',
184  [ $key, $value, $exptime, $flags ]
185  );
186  }
187 
188  return false;
189  }
190 
191  public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
192  return $this->doWrite(
193  $this->cacheIndexes,
194  $this->usesAsyncWritesGivenFlags( $flags ),
195  __FUNCTION__,
196  func_get_args()
197  );
198  }
199 
200  public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
201  return $this->doWrite(
202  $this->cacheIndexes,
203  $this->usesAsyncWritesGivenFlags( $flags ),
204  __FUNCTION__,
205  func_get_args()
206  );
207  }
208 
209  public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
210  // Only need to lock the first cache; also avoids deadlocks
211  return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
212  }
213 
214  public function unlock( $key ) {
215  // Only the first cache is locked
216  return $this->caches[0]->unlock( $key );
217  }
218 
220  $timestamp,
221  callable $progress = null,
222  $limit = INF
223  ) {
224  $ret = false;
225  foreach ( $this->caches as $cache ) {
226  if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
227  $ret = true;
228  }
229  }
230 
231  return $ret;
232  }
233 
234  public function getMulti( array $keys, $flags = 0 ) {
235  // Just iterate over each key in order to handle all the backfill logic
236  $res = [];
237  foreach ( $keys as $key ) {
238  $val = $this->get( $key, $flags );
239  if ( $val !== false ) {
240  $res[$key] = $val;
241  }
242  }
243 
244  return $res;
245  }
246 
247  public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
248  return $this->doWrite(
249  $this->cacheIndexes,
250  $this->usesAsyncWritesGivenFlags( $flags ),
251  __FUNCTION__,
252  func_get_args()
253  );
254  }
255 
256  public function deleteMulti( array $data, $flags = 0 ) {
257  return $this->doWrite(
258  $this->cacheIndexes,
259  $this->usesAsyncWritesGivenFlags( $flags ),
260  __FUNCTION__,
261  func_get_args()
262  );
263  }
264 
265  public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
266  return $this->doWrite(
267  $this->cacheIndexes,
268  $this->usesAsyncWritesGivenFlags( $flags ),
269  __FUNCTION__,
270  func_get_args()
271  );
272  }
273 
274  public function incr( $key, $value = 1, $flags = 0 ) {
275  return $this->doWrite(
276  $this->cacheIndexes,
277  $this->asyncWrites,
278  __FUNCTION__,
279  func_get_args()
280  );
281  }
282 
283  public function decr( $key, $value = 1, $flags = 0 ) {
284  return $this->doWrite(
285  $this->cacheIndexes,
286  $this->asyncWrites,
287  __FUNCTION__,
288  func_get_args()
289  );
290  }
291 
292  public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
293  return $this->doWrite(
294  $this->cacheIndexes,
295  $this->asyncWrites,
296  __FUNCTION__,
297  func_get_args()
298  );
299  }
300 
301  public function getLastError() {
302  return $this->caches[0]->getLastError();
303  }
304 
305  public function clearLastError() {
306  $this->caches[0]->clearLastError();
307  }
308 
318  protected function doWrite( $indexes, $asyncWrites, $method, array $args ) {
319  $ret = true;
320 
321  if ( array_diff( $indexes, [ 0 ] ) && $asyncWrites && $method !== 'merge' ) {
322  // Deep-clone $args to prevent misbehavior when something writes an
323  // object to the BagOStuff then modifies it afterwards, e.g. T168040.
325  }
326 
327  foreach ( $indexes as $i ) {
328  $cache = $this->caches[$i];
329  if ( $i == 0 || !$asyncWrites ) {
330  // First store or in sync mode: write now and get result
331  if ( !$cache->$method( ...$args ) ) {
332  $ret = false;
333  }
334  } else {
335  // Secondary write in async mode: do not block this HTTP request
338  function () use ( $cache, $method, $args, $logger ) {
339  if ( !$cache->$method( ...$args ) ) {
340  $logger->warning( "Async $method op failed" );
341  }
342  }
343  );
344  }
345  }
346 
347  return $ret;
348  }
349 
354  protected function usesAsyncWritesGivenFlags( $flags ) {
355  return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
356  }
357 
358  public function makeKeyInternal( $keyspace, $components ) {
359  return $this->caches[0]->makeKeyInternal( $keyspace, $components );
360  }
361 
362  public function makeKey( $class, ...$components ) {
363  return $this->caches[0]->makeKey( ...func_get_args() );
364  }
365 
366  public function makeGlobalKey( $class, ...$components ) {
367  return $this->caches[0]->makeGlobalKey( ...func_get_args() );
368  }
369 
370  public function addBusyCallback( callable $workCallback ) {
371  $this->caches[0]->addBusyCallback( $workCallback );
372  }
373 
374  public function setNewPreparedValues( array $valueByKey ) {
375  return $this->caches[0]->setNewPreparedValues( $valueByKey );
376  }
377 
378  public function setMockTime( &$time ) {
379  parent::setMockTime( $time );
380  foreach ( $this->caches as $cache ) {
381  $cache->setMockTime( $time );
382  }
383  }
384 }
MultiWriteBagOStuff\$cacheIndexes
int[] $cacheIndexes
List of all backing cache indexes.
Definition: MultiWriteBagOStuff.php:42
MultiWriteBagOStuff\lock
lock( $key, $timeout=6, $expiry=6, $rclass='')
Acquire an advisory lock on a key string.
Definition: MultiWriteBagOStuff.php:209
MultiWriteBagOStuff\doWrite
doWrite( $indexes, $asyncWrites, $method, array $args)
Apply a write method to the backing caches specified by $indexes (in order)
Definition: MultiWriteBagOStuff.php:318
MultiWriteBagOStuff\setDebug
setDebug( $enabled)
Definition: MultiWriteBagOStuff.php:106
MultiWriteBagOStuff\setMulti
setMulti(array $data, $exptime=0, $flags=0)
Batch insertion/replace.
Definition: MultiWriteBagOStuff.php:247
MultiWriteBagOStuff\getLastError
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
Definition: MultiWriteBagOStuff.php:301
MultiWriteBagOStuff\$asyncWrites
bool $asyncWrites
Use async secondary writes.
Definition: MultiWriteBagOStuff.php:40
MultiWriteBagOStuff\clearLastError
clearLastError()
Clear the "last error" registry.
Definition: MultiWriteBagOStuff.php:305
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:191
MultiWriteBagOStuff\decr
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
Definition: MultiWriteBagOStuff.php:283
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:71
MultiWriteBagOStuff\changeTTL
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on a key if it exists.
Definition: MultiWriteBagOStuff.php:200
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:292
$res
$res
Definition: testCompression.php:57
serialize
serialize()
Definition: ApiMessageTrait.php:138
BagOStuff\$logger
LoggerInterface $logger
Definition: BagOStuff.php:73
MultiWriteBagOStuff\addBusyCallback
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
Definition: MultiWriteBagOStuff.php:370
MultiWriteBagOStuff\changeTTLMulti
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple keys that exist.
Definition: MultiWriteBagOStuff.php:265
MultiWriteBagOStuff\deleteMulti
deleteMulti(array $data, $flags=0)
Batch deletion.
Definition: MultiWriteBagOStuff.php:256
MultiWriteBagOStuff\unlock
unlock( $key)
Release an advisory lock on a key string.
Definition: MultiWriteBagOStuff.php:214
MultiWriteBagOStuff\makeGlobalKey
makeGlobalKey( $class,... $components)
Make a global cache key.
Definition: MultiWriteBagOStuff.php:366
BagOStuff\$asyncHandler
callable null $asyncHandler
Definition: BagOStuff.php:76
$args
if( $line===false) $args
Definition: mcc.php:124
MultiWriteBagOStuff\add
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
Definition: MultiWriteBagOStuff.php:167
MultiWriteBagOStuff\incr
incr( $key, $value=1, $flags=0)
Increase stored value of $key by $value while preserving its TTL.
Definition: MultiWriteBagOStuff.php:274
MultiWriteBagOStuff\setMockTime
setMockTime(&$time)
Definition: MultiWriteBagOStuff.php:378
MultiWriteBagOStuff\__construct
__construct( $params)
Stable to call.
Definition: MultiWriteBagOStuff.php:68
BagOStuff\mergeFlagMaps
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
Definition: BagOStuff.php:520
MultiWriteBagOStuff
A cache class that replicates all writes to multiple child caches.
Definition: MultiWriteBagOStuff.php:36
MultiWriteBagOStuff\$caches
BagOStuff[] $caches
Definition: MultiWriteBagOStuff.php:38
MultiWriteBagOStuff\deleteObjectsExpiringBefore
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF)
Delete all objects expiring before a certain date.
Definition: MultiWriteBagOStuff.php:219
unserialize
unserialize( $serialized)
Definition: ApiMessageTrait.php:146
$cache
$cache
Definition: mcc.php:33
MultiWriteBagOStuff\makeKeyInternal
makeKeyInternal( $keyspace, $components)
Construct a cache key.
Definition: MultiWriteBagOStuff.php:358
BagOStuff\fieldHasFlags
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:510
$keys
$keys
Definition: testCompression.php:72
MultiWriteBagOStuff\setNewPreparedValues
setNewPreparedValues(array $valueByKey)
Prepare values for storage and get their serialized sizes, or, estimate those sizes.
Definition: MultiWriteBagOStuff.php:374
MultiWriteBagOStuff\makeKey
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
Definition: MultiWriteBagOStuff.php:362
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:234
MultiWriteBagOStuff\usesAsyncWritesGivenFlags
usesAsyncWritesGivenFlags( $flags)
Definition: MultiWriteBagOStuff.php:354
MultiWriteBagOStuff\$UPGRADE_TTL
static int $UPGRADE_TTL
TTL when a key is copied to a higher cache tier.
Definition: MultiWriteBagOStuff.php:45