MediaWiki master
MultiWriteBagOStuff.php
Go to the documentation of this file.
1<?php
24namespace Wikimedia\ObjectCache;
25
26use InvalidArgumentException;
27use Wikimedia\ObjectFactory\ObjectFactory;
28
42 protected $caches;
43
45 protected $asyncWrites = false;
47 protected $cacheIndexes = [];
48
50 private static $UPGRADE_TTL = 3600;
51
75 public function __construct( $params ) {
76 parent::__construct( $params );
77
78 if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
79 throw new InvalidArgumentException(
80 __METHOD__ . ': "caches" parameter must be an array of caches'
81 );
82 }
83
84 $this->caches = [];
85 foreach ( $params['caches'] as $cacheInfo ) {
86 if ( $cacheInfo instanceof BagOStuff ) {
87 $this->caches[] = $cacheInfo;
88 } else {
89 $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
90 }
91 }
92
93 $this->attrMap = $this->mergeFlagMaps( $this->caches );
94
95 $this->asyncWrites = (
96 isset( $params['replication'] ) &&
97 $params['replication'] === 'async' &&
98 is_callable( $this->asyncHandler )
99 );
100
101 $this->cacheIndexes = array_keys( $this->caches );
102 }
103
104 public function get( $key, $flags = 0 ) {
105 $args = func_get_args();
106
107 if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
108 // If the latest write was a delete(), we do NOT want to fallback
109 // to the other tiers and possibly see the old value. Also, this
110 // is used by merge(), which only needs to hit the primary.
111 return $this->callKeyMethodOnTierCache(
112 0,
113 __FUNCTION__,
114 self::ARG0_KEY,
115 self::RES_NONKEY,
116 $args
117 );
118 }
119
120 $value = false;
121 // backends checked
122 $missIndexes = [];
123 foreach ( $this->cacheIndexes as $i ) {
124 $value = $this->callKeyMethodOnTierCache(
125 $i,
126 __FUNCTION__,
127 self::ARG0_KEY,
128 self::RES_NONKEY,
129 $args
130 );
131 if ( $value !== false ) {
132 break;
133 }
134 $missIndexes[] = $i;
135 }
136
137 if (
138 $value !== false &&
139 $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
140 $missIndexes
141 ) {
142 // Backfill the value to the higher (and often faster/smaller) cache tiers
143 $this->callKeyWriteMethodOnTierCaches(
144 $missIndexes,
145 'set',
146 self::ARG0_KEY,
147 self::RES_NONKEY,
148 [ $key, $value, self::$UPGRADE_TTL ]
149 );
150 }
151
152 return $value;
153 }
154
155 public function set( $key, $value, $exptime = 0, $flags = 0 ) {
156 return $this->callKeyWriteMethodOnTierCaches(
157 $this->cacheIndexes,
158 __FUNCTION__,
159 self::ARG0_KEY,
160 self::RES_NONKEY,
161 func_get_args()
162 );
163 }
164
165 public function delete( $key, $flags = 0 ) {
166 return $this->callKeyWriteMethodOnTierCaches(
167 $this->cacheIndexes,
168 __FUNCTION__,
169 self::ARG0_KEY,
170 self::RES_NONKEY,
171 func_get_args()
172 );
173 }
174
175 public function add( $key, $value, $exptime = 0, $flags = 0 ) {
176 // Try the write to the top-tier cache
177 $ok = $this->callKeyMethodOnTierCache(
178 0,
179 __FUNCTION__,
180 self::ARG0_KEY,
181 self::RES_NONKEY,
182 func_get_args()
183 );
184
185 if ( $ok ) {
186 // Relay the add() using set() if it succeeded. This is meant to handle certain
187 // migration scenarios where the same store might get written to twice for certain
188 // keys. In that case, it makes no sense to return false due to "self-conflicts".
189 $okSecondaries = $this->callKeyWriteMethodOnTierCaches(
190 array_slice( $this->cacheIndexes, 1 ),
191 'set',
192 self::ARG0_KEY,
193 self::RES_NONKEY,
194 [ $key, $value, $exptime, $flags ]
195 );
196 if ( $okSecondaries === false ) {
197 $ok = false;
198 }
199 }
200
201 return $ok;
202 }
203
204 public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
205 return $this->callKeyWriteMethodOnTierCaches(
206 $this->cacheIndexes,
207 __FUNCTION__,
208 self::ARG0_KEY,
209 self::RES_NONKEY,
210 func_get_args()
211 );
212 }
213
214 public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
215 return $this->callKeyWriteMethodOnTierCaches(
216 $this->cacheIndexes,
217 __FUNCTION__,
218 self::ARG0_KEY,
219 self::RES_NONKEY,
220 func_get_args()
221 );
222 }
223
224 public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
225 // Only need to lock the first cache; also avoids deadlocks
226 return $this->callKeyMethodOnTierCache(
227 0,
228 __FUNCTION__,
229 self::ARG0_KEY,
230 self::RES_NONKEY,
231 func_get_args()
232 );
233 }
234
235 public function unlock( $key ) {
236 // Only the first cache is locked
237 return $this->callKeyMethodOnTierCache(
238 0,
239 __FUNCTION__,
240 self::ARG0_KEY,
241 self::RES_NONKEY,
242 func_get_args()
243 );
244 }
245
247 $timestamp,
248 callable $progress = null,
249 $limit = INF,
250 string $tag = null
251 ) {
252 $ret = false;
253 foreach ( $this->caches as $cache ) {
254 if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit, $tag ) ) {
255 $ret = true;
256 }
257 }
258
259 return $ret;
260 }
261
262 public function getMulti( array $keys, $flags = 0 ) {
263 // Just iterate over each key in order to handle all the backfill logic
264 $res = [];
265 foreach ( $keys as $key ) {
266 $val = $this->get( $key, $flags );
267 if ( $val !== false ) {
268 $res[$key] = $val;
269 }
270 }
271
272 return $res;
273 }
274
275 public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
276 return $this->callKeyWriteMethodOnTierCaches(
277 $this->cacheIndexes,
278 __FUNCTION__,
279 self::ARG0_KEYMAP,
280 self::RES_NONKEY,
281 func_get_args()
282 );
283 }
284
285 public function deleteMulti( array $keys, $flags = 0 ) {
286 return $this->callKeyWriteMethodOnTierCaches(
287 $this->cacheIndexes,
288 __FUNCTION__,
289 self::ARG0_KEYARR,
290 self::RES_NONKEY,
291 func_get_args()
292 );
293 }
294
295 public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
296 return $this->callKeyWriteMethodOnTierCaches(
297 $this->cacheIndexes,
298 __FUNCTION__,
299 self::ARG0_KEYARR,
300 self::RES_NONKEY,
301 func_get_args()
302 );
303 }
304
305 public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
306 return $this->callKeyWriteMethodOnTierCaches(
307 $this->cacheIndexes,
308 __FUNCTION__,
309 self::ARG0_KEY,
310 self::RES_NONKEY,
311 func_get_args()
312 );
313 }
314
315 public function setMockTime( &$time ) {
316 parent::setMockTime( $time );
317 foreach ( $this->caches as $cache ) {
318 $cache->setMockTime( $time );
319 }
320 }
321
333 private function callKeyMethodOnTierCache( $index, $method, $arg0Sig, $rvSig, array $args ) {
334 return $this->caches[$index]->proxyCall( $method, $arg0Sig, $rvSig, $args, $this );
335 }
336
348 private function callKeyWriteMethodOnTierCaches(
349 array $indexes,
350 $method,
351 $arg0Sig,
352 $resSig,
353 array $args
354 ) {
355 $res = null;
356
357 if ( $this->asyncWrites && array_diff( $indexes, [ 0 ] ) && $method !== 'merge' ) {
358 // Deep-clone $args to prevent misbehavior when something writes an
359 // object to the BagOStuff then modifies it afterwards, e.g. T168040.
360 $args = unserialize( serialize( $args ) );
361 }
362
363 foreach ( $indexes as $i ) {
364 $cache = $this->caches[$i];
365
366 if ( $i == 0 || !$this->asyncWrites ) {
367 // Tier 0 store or in sync mode: write synchronously and get result
368 $storeRes = $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
369 if ( $storeRes === false ) {
370 $res = false;
371 } elseif ( $res === null ) {
372 // first synchronous result
373 $res = $storeRes;
374 }
375 } else {
376 // Secondary write in async mode: do not block this HTTP request
378 function () use ( $cache, $method, $arg0Sig, $resSig, $args ) {
379 $cache->proxyCall( $method, $arg0Sig, $resSig, $args, $this );
380 }
381 );
382 }
383 }
384
385 return $res;
386 }
387}
388
390class_alias( MultiWriteBagOStuff::class, 'MultiWriteBagOStuff' );
array $params
The job parameters.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:88
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
A cache class that replicates all writes to multiple child caches.
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple items.
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
deleteObjectsExpiringBefore( $timestamp, callable $progress=null, $limit=INF, string $tag=null)
Delete all objects expiring before a certain date.
int[] $cacheIndexes
List of all backing cache indexes.
deleteMulti(array $keys, $flags=0)
Delete a batch of items.
setMulti(array $valueByKey, $exptime=0, $flags=0)
Set a batch of items.
getMulti(array $keys, $flags=0)
Get a batch of items.
unlock( $key)
Release an advisory lock on a key string.
lock( $key, $timeout=6, $exptime=6, $rclass='')
Acquire an advisory lock on a key string, exclusive to the caller.
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on an item.
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.
BagOStuff[] $caches
Backing cache stores in order of highest to lowest tier.
bool $asyncWrites
Use async secondary writes.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)