MediaWiki master
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 $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}
array $params
The job parameters.
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.
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.
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.