MediaWiki  master
ReplicatedBagOStuff.php
Go to the documentation of this file.
1 <?php
21 use Wikimedia\ObjectFactory\ObjectFactory;
22 
36  private $writeStore;
38  private $readStore;
39 
43  private $lastKeyWrites = [];
44 
46  private const MAX_WRITE_DELAY = 5;
47 
60  public function __construct( $params ) {
61  parent::__construct( $params );
62 
63  if ( !isset( $params['writeFactory'] ) ) {
64  throw new InvalidArgumentException(
65  __METHOD__ . ': the "writeFactory" parameter is required' );
66  } elseif ( !isset( $params['readFactory'] ) ) {
67  throw new InvalidArgumentException(
68  __METHOD__ . ': the "readFactory" parameter is required' );
69  }
70 
71  $this->consistencyWindow = $params['sessionConsistencyWindow'] ?? self::MAX_WRITE_DELAY;
72  $this->writeStore = ( $params['writeFactory'] instanceof BagOStuff )
73  ? $params['writeFactory']
74  : ObjectFactory::getObjectFromSpec( $params['writeFactory'] );
75  $this->readStore = ( $params['readFactory'] instanceof BagOStuff )
76  ? $params['readFactory']
77  : ObjectFactory::getObjectFromSpec( $params['readFactory'] );
78  $this->attrMap = $this->mergeFlagMaps( [ $this->readStore, $this->writeStore ] );
79  }
80 
81  public function get( $key, $flags = 0 ) {
82  $store = (
83  $this->hadRecentSessionWrite( [ $key ] ) ||
84  $this->fieldHasFlags( $flags, self::READ_LATEST )
85  )
86  // Try to maintain session consistency and respect READ_LATEST
87  ? $this->writeStore
88  // Otherwise, just use the default "read" store
89  : $this->readStore;
90 
91  return $store->proxyCall(
92  __FUNCTION__,
93  self::ARG0_KEY,
94  self::RES_NONKEY,
95  func_get_args(),
96  $this
97  );
98  }
99 
100  public function set( $key, $value, $exptime = 0, $flags = 0 ) {
101  $this->remarkRecentSessionWrite( [ $key ] );
102 
103  return $this->writeStore->proxyCall(
104  __FUNCTION__,
105  self::ARG0_KEY,
106  self::RES_NONKEY,
107  func_get_args(),
108  $this
109  );
110  }
111 
112  public function delete( $key, $flags = 0 ) {
113  $this->remarkRecentSessionWrite( [ $key ] );
114 
115  return $this->writeStore->proxyCall(
116  __FUNCTION__,
117  self::ARG0_KEY,
118  self::RES_NONKEY,
119  func_get_args(),
120  $this
121  );
122  }
123 
124  public function add( $key, $value, $exptime = 0, $flags = 0 ) {
125  $this->remarkRecentSessionWrite( [ $key ] );
126 
127  return $this->writeStore->proxyCall(
128  __FUNCTION__,
129  self::ARG0_KEY,
130  self::RES_NONKEY,
131  func_get_args(),
132  $this
133  );
134  }
135 
136  public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
137  $this->remarkRecentSessionWrite( [ $key ] );
138 
139  return $this->writeStore->proxyCall(
140  __FUNCTION__,
141  self::ARG0_KEY,
142  self::RES_NONKEY,
143  func_get_args(),
144  $this
145  );
146  }
147 
148  public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
149  $this->remarkRecentSessionWrite( [ $key ] );
150 
151  return $this->writeStore->proxyCall(
152  __FUNCTION__,
153  self::ARG0_KEY,
154  self::RES_NONKEY,
155  func_get_args(),
156  $this
157  );
158  }
159 
160  public function lock( $key, $timeout = 6, $exptime = 6, $rclass = '' ) {
161  return $this->writeStore->proxyCall(
162  __FUNCTION__,
163  self::ARG0_KEY,
164  self::RES_NONKEY,
165  func_get_args(),
166  $this
167  );
168  }
169 
170  public function unlock( $key ) {
171  return $this->writeStore->proxyCall(
172  __FUNCTION__,
173  self::ARG0_KEY,
174  self::RES_NONKEY,
175  func_get_args(),
176  $this
177  );
178  }
179 
181  $timestamp,
182  callable $progress = null,
183  $limit = INF,
184  string $tag = null
185  ) {
186  return $this->writeStore->proxyCall(
187  __FUNCTION__,
188  self::ARG0_NONKEY,
189  self::RES_NONKEY,
190  func_get_args(),
191  $this
192  );
193  }
194 
195  public function getMulti( array $keys, $flags = 0 ) {
196  $store = (
197  $this->hadRecentSessionWrite( $keys ) ||
198  $this->fieldHasFlags( $flags, self::READ_LATEST )
199  )
200  // Try to maintain session consistency and respect READ_LATEST
201  ? $this->writeStore
202  // Otherwise, just use the default "read" store
203  : $this->readStore;
204 
205  return $store->proxyCall(
206  __FUNCTION__,
207  self::ARG0_KEYARR,
208  self::RES_KEYMAP,
209  func_get_args(),
210  $this
211  );
212  }
213 
214  public function setMulti( array $valueByKey, $exptime = 0, $flags = 0 ) {
215  $this->remarkRecentSessionWrite( array_keys( $valueByKey ) );
216 
217  return $this->writeStore->proxyCall(
218  __FUNCTION__,
219  self::ARG0_KEYMAP,
220  self::RES_KEYMAP,
221  func_get_args(),
222  $this
223  );
224  }
225 
226  public function deleteMulti( array $keys, $flags = 0 ) {
227  $this->remarkRecentSessionWrite( $keys );
228 
229  return $this->writeStore->proxyCall(
230  __FUNCTION__,
231  self::ARG0_KEYARR,
232  self::RES_NONKEY,
233  func_get_args(),
234  $this
235  );
236  }
237 
238  public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
239  $this->remarkRecentSessionWrite( $keys );
240 
241  return $this->writeStore->proxyCall(
242  __FUNCTION__,
243  self::ARG0_KEYARR,
244  self::RES_NONKEY,
245  func_get_args(),
246  $this
247  );
248  }
249 
250  public function incr( $key, $value = 1, $flags = 0 ) {
251  $this->remarkRecentSessionWrite( [ $key ] );
252 
253  return $this->writeStore->proxyCall(
254  __FUNCTION__,
255  self::ARG0_KEY,
256  self::RES_NONKEY,
257  func_get_args(),
258  $this
259  );
260  }
261 
262  public function decr( $key, $value = 1, $flags = 0 ) {
263  $this->remarkRecentSessionWrite( [ $key ] );
264 
265  return $this->writeStore->proxyCall(
266  __FUNCTION__,
267  self::ARG0_KEY,
268  self::RES_NONKEY,
269  func_get_args(),
270  $this
271  );
272  }
273 
274  public function incrWithInit( $key, $exptime, $step = 1, $init = null, $flags = 0 ) {
275  $this->remarkRecentSessionWrite( [ $key ] );
276 
277  return $this->writeStore->proxyCall(
278  __FUNCTION__,
279  self::ARG0_KEY,
280  self::RES_NONKEY,
281  func_get_args(),
282  $this
283  );
284  }
285 
286  public function makeKeyInternal( $keyspace, $components ) {
287  return $this->genericKeyFromComponents( $keyspace, ...$components );
288  }
289 
290  public function makeKey( $collection, ...$components ) {
291  return $this->genericKeyFromComponents( $this->keyspace, $collection, ...$components );
292  }
293 
294  public function makeGlobalKey( $collection, ...$components ) {
295  return $this->genericKeyFromComponents( self::GLOBAL_KEYSPACE, $collection, ...$components );
296  }
297 
298  protected function convertGenericKey( $key ) {
299  return $key; // short-circuit; already uses "generic" keys
300  }
301 
302  public function addBusyCallback( callable $workCallback ) {
303  return $this->writeStore->addBusyCallback( $workCallback );
304  }
305 
306  public function setNewPreparedValues( array $valueByKey ) {
307  return $this->writeStore->proxyCall(
308  __FUNCTION__,
309  self::ARG0_KEYMAP,
310  self::RES_NONKEY,
311  func_get_args(),
312  $this
313  );
314  }
315 
316  public function setMockTime( &$time ) {
317  parent::setMockTime( $time );
318  $this->writeStore->setMockTime( $time );
319  $this->readStore->setMockTime( $time );
320  }
321 
326  private function hadRecentSessionWrite( array $keys ) {
327  $now = $this->getCurrentTime();
328  foreach ( $keys as $key ) {
329  $ts = $this->lastKeyWrites[$key] ?? 0;
330  if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) {
331  return true;
332  }
333  }
334 
335  return false;
336  }
337 
341  private function remarkRecentSessionWrite( array $keys ) {
342  $now = $this->getCurrentTime();
343  foreach ( $keys as $key ) {
344  unset( $this->lastKeyWrites[$key] ); // move to the end
345  $this->lastKeyWrites[$key] = $now;
346  }
347  // Prune out the map if the first key is obsolete
348  if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) {
349  $this->lastKeyWrites = array_filter(
350  $this->lastKeyWrites,
351  function ( $timestamp ) use ( $now ) {
352  return ( ( $now - $timestamp ) <= $this->consistencyWindow );
353  }
354  );
355  }
356  }
357 }
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:87
mergeFlagMaps(array $bags)
Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map.
Definition: BagOStuff.php:633
genericKeyFromComponents(... $components)
At a minimum, there must be a keyspace and collection name component.
Definition: BagOStuff.php:715
string $keyspace
Default keyspace; used by makeKey()
Definition: BagOStuff.php:104
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:623
getCurrentTime()
Definition: BagOStuff.php:846
A cache class that directs writes to one set of servers and reads to another.
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.
__construct( $params)
Constructor.
changeTTLMulti(array $keys, $exptime, $flags=0)
Change the expiration of multiple keys that exist.
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.
lock( $key, $timeout=6, $exptime=6, $rclass='')
Acquire an advisory lock on a key string, exclusive to the caller.
incr( $key, $value=1, $flags=0)
Increase stored value of $key by $value while preserving its TTL.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
convertGenericKey( $key)
Convert a "generic" reversible cache key into one for this cache.
deleteMulti(array $keys, $flags=0)
Batch deletion.
remarkRecentSessionWrite(array $keys)
getMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on a key if it exists.
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
setNewPreparedValues(array $valueByKey)
Make a "generic" reversible cache key from the given components.
setMulti(array $valueByKey, $exptime=0, $flags=0)
Batch insertion/replace.
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
hadRecentSessionWrite(array $keys)
makeKeyInternal( $keyspace, $components)
Make a cache key for the given keyspace and components.
int $consistencyWindow
Seconds to read from the master source for a key after writing to it.
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
float[] $lastKeyWrites
Map of (key => UNIX timestamp)
unlock( $key)
Release an advisory lock on a key string.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.