MediaWiki REL1_35
MultiWriteBagOStuff.php
Go to the documentation of this file.
1<?php
23use 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 // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
380 $cache->setMockTime( $time );
381 }
382 }
383}
serialize()
unserialize( $serialized)
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:71
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
LoggerInterface $logger
Definition BagOStuff.php:73
fieldHasFlags( $field, $flags)
callable null $asyncHandler
Definition BagOStuff.php:76
A cache class that replicates all writes to multiple child caches.
setMulti(array $data, $exptime=0, $flags=0)
Batch insertion/replace.
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple keys that exist.
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.
doWrite( $indexes, $asyncWrites, $method, array $args)
Apply a write method to the backing caches specified by $indexes (in order)
static int $UPGRADE_TTL
TTL when a key is copied to a higher cache tier.
int[] $cacheIndexes
List of all backing cache indexes.
makeGlobalKey( $class,... $components)
Make a global cache key.
getLastError()
Get the "last error" registered; clearLastError() should be called manually.
__construct( $params)
Stable to call.
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
clearLastError()
Clear the "last error" registry.
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 a key if it exists.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
bool $asyncWrites
Use async secondary writes.
makeKeyInternal( $keyspace, $args)
Construct a cache key.
lock( $key, $timeout=6, $expiry=6, $rclass='')
Acquire an advisory lock on a key string.
setNewPreparedValues(array $valueByKey)
Prepare values for storage and get their serialized sizes, or, estimate those sizes.
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF)
Delete all objects expiring before a certain date.
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.
deleteMulti(array $data, $flags=0)
Batch deletion.
$cache
Definition mcc.php:33
if( $line===false) $args
Definition mcc.php:124