MediaWiki REL1_39
MultiWriteBagOStuff.php
Go to the documentation of this file.
1<?php
23use 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 setNewPreparedValues( array $valueByKey ) {
361 return $this->callKeyMethodOnTierCache(
362 0,
363 __FUNCTION__,
364 self::ARG0_KEYMAP,
365 self::RES_NONKEY,
366 func_get_args()
367 );
368 }
369
370 public function setMockTime( &$time ) {
371 parent::setMockTime( $time );
372 foreach ( $this->caches as $cache ) {
373 $cache->setMockTime( $time );
374 }
375 }
376
387 private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
388 return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args, $this );
389 }
390
401 private function callKeyWriteMethodOnTierCaches(
402 array $indexes,
403 $method,
404 $arg0Sig,
405 $resSig,
406 array $args
407 ) {
408 $res = null;
409
410 if ( $this->asyncWrites && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
411 // Deep-clone $args to prevent misbehavior when something writes an
412 // object to the BagOStuff then modifies it afterwards, e.g. T168040.
414 }
415
416 foreach ( $indexes as $i ) {
417 $cache = $this->caches[$i];
418
419 if ( $i == 0 || !$this->asyncWrites ) {
420 // Tier 0 store or in sync mode: write synchronously and get result
421 $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
422 if ( $storeRes === false ) {
423 $res = false;
424 } elseif ( $res === null ) {
425 // first synchronous result
426 $res = $storeRes;
427 }
428 } else {
429 // Secondary write in async mode: do not block this HTTP request
431 function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
432 $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
433 }
434 );
435 }
436 }
437
438 return $res;
439 }
440}
serialize()
unserialize( $serialized)
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.
genericKeyFromComponents(... $components)
At a minimum, there must be a keyspace and collection name component.
string $keyspace
Default keyspace; used by makeKey()
fieldHasFlags( $field, $flags)
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)
Make a cache key for the given keyspace and components.
setNewPreparedValues(array $valueByKey)
Stage a set of new key values for storage and estimate the amount of bytes needed.
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