MediaWiki  master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\ObjectFactory\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 intending 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 
97  $this->attrMap = $this->mergeFlagMaps( $this->caches );
98 
99  $this->asyncWrites = (
100  isset( $params['replication'] ) &&
101  $params['replication'] === 'async' &&
102  is_callable( $this->asyncHandler )
103  );
104 
105  $this->cacheIndexes = array_keys( $this->caches );
106  }
107 
108  public function get( $key, $flags = 0 ) {
109  $args = func_get_args();
110 
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->callKeyMethodOnTierCache(
116  0,
117  __FUNCTION__,
118  self::ARG0_KEY,
119  self::RES_NONKEY,
120  $args
121  );
122  }
123 
124  $value = false;
125  // backends checked
126  $missIndexes = [];
127  foreach ( $this->cacheIndexes as $i ) {
128  $value = $this->callKeyMethodOnTierCache(
129  $i,
130  __FUNCTION__,
131  self::ARG0_KEY,
132  self::RES_NONKEY,
133  $args
134  );
135  if ( $value !== false ) {
136  break;
137  }
138  $missIndexes[] = $i;
139  }
140 
141  if (
142  $value !== false &&
143  $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
144  $missIndexes
145  ) {
146  // Backfill the value to the higher (and often faster/smaller) cache tiers
147  $this->callKeyWriteMethodOnTierCaches(
148  $missIndexes,
149  'set',
150  self::ARG0_KEY,
151  self::RES_NONKEY,
152  [ $key, $value, self::$UPGRADE_TTL ]
153  );
154  }
155 
156  return $value;
157  }
158 
159  public function set( $key, $value, $exptime = 0, $flags = 0 ) {
160  return $this->callKeyWriteMethodOnTierCaches(
161  $this->cacheIndexes,
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  __FUNCTION__,
173  self::ARG0_KEY,
174  self::RES_NONKEY,
175  func_get_args()
176  );
177  }
178 
179  public function add( $key, $value, $exptime = 0, $flags = 0 ) {
180  // Try the write to the top-tier cache
181  $ok = $this->callKeyMethodOnTierCache(
182  0,
183  __FUNCTION__,
184  self::ARG0_KEY,
185  self::RES_NONKEY,
186  func_get_args()
187  );
188 
189  if ( $ok ) {
190  // Relay the add() using set() if it succeeded. This is meant to handle certain
191  // migration scenarios where the same store might get written to twice for certain
192  // keys. In that case, it makes no sense to return false due to "self-conflicts".
193  $okSecondaries = $this->callKeyWriteMethodOnTierCaches(
194  array_slice( $this->cacheIndexes, 1 ),
195  'set',
196  self::ARG0_KEY,
197  self::RES_NONKEY,
198  [ $key, $value, $exptime, $flags ]
199  );
200  if ( $okSecondaries === false ) {
201  $ok = false;
202  }
203  }
204 
205  return $ok;
206  }
207 
208  public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
209  return $this->callKeyWriteMethodOnTierCaches(
210  $this->cacheIndexes,
211  __FUNCTION__,
212  self::ARG0_KEY,
213  self::RES_NONKEY,
214  func_get_args()
215  );
216  }
217 
218  public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
219  return $this->callKeyWriteMethodOnTierCaches(
220  $this->cacheIndexes,
221  __FUNCTION__,
222  self::ARG0_KEY,
223  self::RES_NONKEY,
224  func_get_args()
225  );
226  }
227 
228  public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
229  // Only need to lock the first cache; also avoids deadlocks
230  return $this->callKeyMethodOnTierCache(
231  0,
232  __FUNCTION__,
233  self::ARG0_KEY,
234  self::RES_NONKEY,
235  func_get_args()
236  );
237  }
238 
239  public function unlock( $key ) {
240  // Only the first cache is locked
241  return $this->callKeyMethodOnTierCache(
242  0,
243  __FUNCTION__,
244  self::ARG0_KEY,
245  self::RES_NONKEY,
246  func_get_args()
247  );
248  }
249 
251  $timestamp,
252  callable $progress = null,
253  $limit = INF,
254  string $tag = null
255  ) {
256  $ret = false;
257  foreach ( $this->caches as $cache ) {
258  if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag ) ) {
259  $ret = true;
260  }
261  }
262 
263  return $ret;
264  }
265 
266  public function getMulti( array $keys, $flags = 0 ) {
267  // Just iterate over each key in order to handle all the backfill logic
268  $res = [];
269  foreach ( $keys as $key ) {
270  $val = $this->get( $key, $flags );
271  if ( $val !== false ) {
272  $res[$key] = $val;
273  }
274  }
275 
276  return $res;
277  }
278 
279  public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
280  return $this->callKeyWriteMethodOnTierCaches(
281  $this->cacheIndexes,
282  __FUNCTION__,
283  self::ARG0_KEYMAP,
284  self::RES_NONKEY,
285  func_get_args()
286  );
287  }
288 
289  public function deleteMulti( array $keys, $flags = 0 ) {
290  return $this->callKeyWriteMethodOnTierCaches(
291  $this->cacheIndexes,
292  __FUNCTION__,
293  self::ARG0_KEYARR,
294  self::RES_NONKEY,
295  func_get_args()
296  );
297  }
298 
299  public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
300  return $this->callKeyWriteMethodOnTierCaches(
301  $this->cacheIndexes,
302  __FUNCTION__,
303  self::ARG0_KEYARR,
304  self::RES_NONKEY,
305  func_get_args()
306  );
307  }
308 
309  public function incr( $key, $value = 1, $flags = 0 ) {
310  return $this->callKeyWriteMethodOnTierCaches(
311  $this->cacheIndexes,
312  __FUNCTION__,
313  self::ARG0_KEY,
314  self::RES_NONKEY,
315  func_get_args()
316  );
317  }
318 
319  public function decr( $key, $value = 1, $flags = 0 ) {
320  return $this->callKeyWriteMethodOnTierCaches(
321  $this->cacheIndexes,
322  __FUNCTION__,
323  self::ARG0_KEY,
324  self::RES_NONKEY,
325  func_get_args()
326  );
327  }
328 
329  public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
330  return $this->callKeyWriteMethodOnTierCaches(
331  $this->cacheIndexes,
332  __FUNCTION__,
333  self::ARG0_KEY,
334  self::RES_NONKEY,
335  func_get_args()
336  );
337  }
338 
339  public function makeKeyInternal( $keyspace, $components ) {
340  return $this->genericKeyFromComponents( $keyspace, ...$components );
341  }
342 
343  public function makeKey( $collection, ...$components ) {
344  return $this->genericKeyFromComponents( $this->keyspace, $collection, ...$components );
345  }
346 
347  public function makeGlobalKey( $collection, ...$components ) {
348  return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $collection, ...$components );
349  }
350 
351  protected function convertGenericKey( $key ) {
352  // short-circuit; already uses "generic" keys
353  return $key;
354  }
355 
356  public function addBusyCallback( callable $workCallback ) {
357  $this->caches[0]->addBusyCallback( $workCallback );
358  }
359 
360  public function setMockTime( &$time ) {
361  parent::setMockTime( $time );
362  foreach ( $this->caches as $cache ) {
363  $cache->setMockTime( $time );
364  }
365  }
366 
377  private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
378  return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args, $this );
379  }
380 
391  private function callKeyWriteMethodOnTierCaches(
392  array $indexes,
393  $method,
394  $arg0Sig,
395  $resSig,
396  array $args
397  ) {
398  $res = null;
399 
400  if ( $this->asyncWrites && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
401  // Deep-clone $args to prevent misbehavior when something writes an
402  // object to the BagOStuff then modifies it afterwards, e.g. T168040.
403  $args = unserialize( serialize( $args ) );
404  }
405 
406  foreach ( $indexes as $i ) {
407  $cache = $this->caches[$i];
408 
409  if ( $i == 0 || !$this->asyncWrites ) {
410  // Tier 0 store or in sync mode: write synchronously and get result
411  $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
412  if ( $storeRes === false ) {
413  $res = false;
414  } elseif ( $res === null ) {
415  // first synchronous result
416  $res = $storeRes;
417  }
418  } else {
419  // Secondary write in async mode: do not block this HTTP request
421  function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
422  $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
423  }
424  );
425  }
426  }
427 
428  return $res;
429  }
430 }
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
Definition: BagOStuff.php:622
genericKeyFromComponents(... $components)
At a minimum, there must be a keyspace and collection name component.
Definition: BagOStuff.php:703
string $keyspace
Default keyspace; used by makeKey()
Definition: BagOStuff.php:101
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:612
callable null $asyncHandler
Definition: BagOStuff.php:91
A cache class that replicates all writes to multiple child caches.
deleteMulti(array $keys, $flags=0)
Delete a batch of items.
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple items.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
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.
int[] $cacheIndexes
List of all backing cache indexes.
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on an item.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF, string $tag=null)
Delete all objects expiring before a certain date.
BagOStuff[] $caches
Backing cache stores in order of highest to lowest tier.
bool $asyncWrites
Use async secondary writes.
setMulti(array $valueByKey, $exptime=0, $flags=0)
Set a batch of items.
lock( $key, $timeout=6, $exptime=6, $rclass='')
Acquire an advisory lock on a key string, exclusive to the caller.
convertGenericKey( $key)
Convert a "generic" reversible cache key into one for this cache.
makeKeyInternal( $keyspace, $components)
getMulti(array $keys, $flags=0)
Get a batch of items.
incr( $key, $value=1, $flags=0)
Increase stored value of $key by $value while preserving its TTL.
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
unlock( $key)
Release an advisory lock on a key string.
$cache
Definition: mcc.php:33
if( $line===false) $args
Definition: mcc.php:124