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