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  $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
84  }
85  }
86 
87  $this->attrMap = $this->mergeFlagMaps( $this->caches );
88 
89  $this->asyncWrites = (
90  isset( $params['replication'] ) &&
91  $params['replication'] === 'async' &&
92  is_callable( $this->asyncHandler )
93  );
94 
95  $this->cacheIndexes = array_keys( $this->caches );
96  }
97 
98  public function get( $key, $flags = 0 ) {
99  $args = func_get_args();
100 
101  if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
102  // If the latest write was a delete(), we do NOT want to fallback
103  // to the other tiers and possibly see the old value. Also, this
104  // is used by merge(), which only needs to hit the primary.
105  return $this->callKeyMethodOnTierCache(
106  0,
107  __FUNCTION__,
108  self::ARG0_KEY,
109  self::RES_NONKEY,
110  $args
111  );
112  }
113 
114  $value = false;
115  // backends checked
116  $missIndexes = [];
117  foreach ( $this->cacheIndexes as $i ) {
118  $value = $this->callKeyMethodOnTierCache(
119  $i,
120  __FUNCTION__,
121  self::ARG0_KEY,
122  self::RES_NONKEY,
123  $args
124  );
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->callKeyWriteMethodOnTierCaches(
138  $missIndexes,
139  'set',
140  self::ARG0_KEY,
141  self::RES_NONKEY,
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->callKeyWriteMethodOnTierCaches(
151  $this->cacheIndexes,
152  __FUNCTION__,
153  self::ARG0_KEY,
154  self::RES_NONKEY,
155  func_get_args()
156  );
157  }
158 
159  public function delete( $key, $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 add( $key, $value, $exptime = 0, $flags = 0 ) {
170  // Try the write to the top-tier cache
171  $ok = $this->callKeyMethodOnTierCache(
172  0,
173  __FUNCTION__,
174  self::ARG0_KEY,
175  self::RES_NONKEY,
176  func_get_args()
177  );
178 
179  if ( $ok ) {
180  // Relay the add() using set() if it succeeded. This is meant to handle certain
181  // migration scenarios where the same store might get written to twice for certain
182  // keys. In that case, it makes no sense to return false due to "self-conflicts".
183  $okSecondaries = $this->callKeyWriteMethodOnTierCaches(
184  array_slice( $this->cacheIndexes, 1 ),
185  'set',
186  self::ARG0_KEY,
187  self::RES_NONKEY,
188  [ $key, $value, $exptime, $flags ]
189  );
190  if ( $okSecondaries === false ) {
191  $ok = false;
192  }
193  }
194 
195  return $ok;
196  }
197 
198  public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
199  return $this->callKeyWriteMethodOnTierCaches(
200  $this->cacheIndexes,
201  __FUNCTION__,
202  self::ARG0_KEY,
203  self::RES_NONKEY,
204  func_get_args()
205  );
206  }
207 
208  public function changeTTL( $key, $exptime = 0, $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 lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
219  // Only need to lock the first cache; also avoids deadlocks
220  return $this->callKeyMethodOnTierCache(
221  0,
222  __FUNCTION__,
223  self::ARG0_KEY,
224  self::RES_NONKEY,
225  func_get_args()
226  );
227  }
228 
229  public function unlock( $key ) {
230  // Only the first cache is locked
231  return $this->callKeyMethodOnTierCache(
232  0,
233  __FUNCTION__,
234  self::ARG0_KEY,
235  self::RES_NONKEY,
236  func_get_args()
237  );
238  }
239 
241  $timestamp,
242  callable $progress = null,
243  $limit = INF,
244  string $tag = null
245  ) {
246  $ret = false;
247  foreach ( $this->caches as $cache ) {
248  if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag ) ) {
249  $ret = true;
250  }
251  }
252 
253  return $ret;
254  }
255 
256  public function getMulti( array $keys, $flags = 0 ) {
257  // Just iterate over each key in order to handle all the backfill logic
258  $res = [];
259  foreach ( $keys as $key ) {
260  $val = $this->get( $key, $flags );
261  if ( $val !== false ) {
262  $res[$key] = $val;
263  }
264  }
265 
266  return $res;
267  }
268 
269  public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
270  return $this->callKeyWriteMethodOnTierCaches(
271  $this->cacheIndexes,
272  __FUNCTION__,
273  self::ARG0_KEYMAP,
274  self::RES_NONKEY,
275  func_get_args()
276  );
277  }
278 
279  public function deleteMulti( array $keys, $flags = 0 ) {
280  return $this->callKeyWriteMethodOnTierCaches(
281  $this->cacheIndexes,
282  __FUNCTION__,
283  self::ARG0_KEYARR,
284  self::RES_NONKEY,
285  func_get_args()
286  );
287  }
288 
289  public function changeTTLMulti( array $keys, $exptime, $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 incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
300  return $this->callKeyWriteMethodOnTierCaches(
301  $this->cacheIndexes,
302  __FUNCTION__,
303  self::ARG0_KEY,
304  self::RES_NONKEY,
305  func_get_args()
306  );
307  }
308 
309  public function setMockTime( &$time ) {
310  parent::setMockTime( $time );
311  foreach ( $this->caches as $cache ) {
312  $cache->setMockTime( $time );
313  }
314  }
315 
326  private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
327  return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args, $this );
328  }
329 
340  private function callKeyWriteMethodOnTierCaches(
341  array $indexes,
342  $method,
343  $arg0Sig,
344  $resSig,
345  array $args
346  ) {
347  $res = null;
348 
349  if ( $this->asyncWrites && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
350  // Deep-clone $args to prevent misbehavior when something writes an
351  // object to the BagOStuff then modifies it afterwards, e.g. T168040.
352  $args = unserialize( serialize( $args ) );
353  }
354 
355  foreach ( $indexes as $i ) {
356  $cache = $this->caches[$i];
357 
358  if ( $i == 0 || !$this->asyncWrites ) {
359  // Tier 0 store or in sync mode: write synchronously and get result
360  $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
361  if ( $storeRes === false ) {
362  $res = false;
363  } elseif ( $res === null ) {
364  // first synchronous result
365  $res = $storeRes;
366  }
367  } else {
368  // Secondary write in async mode: do not block this HTTP request
370  function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
371  $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
372  }
373  );
374  }
375  }
376 
377  return $res;
378  }
379 }
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:601
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:591
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.
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.
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
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.
getMulti(array $keys, $flags=0)
Get a batch of items.
unlock( $key)
Release an advisory lock on a key string.