MediaWiki  master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1 <?php
24 
37  protected $caches;
39  protected $asyncWrites = false;
41  protected $cacheIndexes = [];
42 
44  private static $UPGRADE_TTL = 3600;
45 
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 
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 
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
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 
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 }
addBusyCallback(callable $workCallback)
changeTTLMulti(array $keys, $exptime, $flags=0)
add( $key, $value, $exptime=0, $flags=0)
incr( $key, $value=1, $flags=0)
serialize()
makeGlobalKey( $class,... $components)
Make a global cache key.
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF)
makeKeyInternal( $keyspace, $args)
if( $line===false) $args
Definition: cdb.php:64
decr( $key, $value=1, $flags=0)
static int $UPGRADE_TTL
TTL when a key is copied to a higher cache tier.
lock( $key, $timeout=6, $expiry=6, $rclass='')
getMulti(array $keys, $flags=0)
doWrite( $indexes, $asyncWrites, $method, array $args)
Apply a write method to the backing caches specified by $indexes (in order)
A cache class that replicates all writes to multiple child caches.
changeTTL( $key, $exptime=0, $flags=0)
incrWithInit( $key, $exptime, $value=1, $init=null, $flags=0)
bool $asyncWrites
Use async secondary writes.
__construct( $params)
$params include:
$cache
Definition: mcc.php:33
unserialize( $serialized)
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map...
Definition: BagOStuff.php:503
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
makeKey( $class,... $components)
Make a cache key, scoped to this instance&#39;s keyspace.
setMulti(array $data, $exptime=0, $flags=0)
int [] $cacheIndexes
List of all backing cache indexes.
LoggerInterface $logger
Definition: BagOStuff.php:65
deleteMulti(array $data, $flags=0)
callable null $asyncHandler
Definition: BagOStuff.php:68
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:493