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 
41  private $consistencyWindow;
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  // short-circuit; already uses "generic" keys
300  return $key;
301  }
302 
303  public function addBusyCallback( callable $workCallback ) {
304  return $this->writeStore->addBusyCallback( $workCallback );
305  }
306 
307  public function setMockTime( &$time ) {
308  parent::setMockTime( $time );
309  $this->writeStore->setMockTime( $time );
310  $this->readStore->setMockTime( $time );
311  }
312 
317  private function hadRecentSessionWrite( array $keys ) {
318  $now = $this->getCurrentTime();
319  foreach ( $keys as $key ) {
320  $ts = $this->lastKeyWrites[$key] ?? 0;
321  if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) {
322  return true;
323  }
324  }
325 
326  return false;
327  }
328 
332  private function remarkRecentSessionWrite( array $keys ) {
333  $now = $this->getCurrentTime();
334  foreach ( $keys as $key ) {
335  // move to the end
336  unset( $this->lastKeyWrites[$key] );
337  $this->lastKeyWrites[$key] = $now;
338  }
339  // Prune out the map if the first key is obsolete
340  if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) {
341  $this->lastKeyWrites = array_filter(
342  $this->lastKeyWrites,
343  function ( $timestamp ) use ( $now ) {
344  return ( ( $now - $timestamp ) <= $this->consistencyWindow );
345  }
346  );
347  }
348  }
349 }
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.
Definition: BagOStuff.php:622
genericKeyFromComponents(... $components)
At a minimum, there must be a keyspace and collection name component.
Definition: BagOStuff.php:703
string $keyspace
Default keyspace; used by makeKey()
Definition: BagOStuff.php:101
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:612
getCurrentTime()
Definition: BagOStuff.php:834
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 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.
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)
Delete a batch of items.
getMulti(array $keys, $flags=0)
Get a batch of items.
changeTTL( $key, $exptime=0, $flags=0)
Change the expiration on an item.
decr( $key, $value=1, $flags=0)
Decrease stored value of $key by $value while preserving its TTL.
setMulti(array $valueByKey, $exptime=0, $flags=0)
Set a batch of items.
addBusyCallback(callable $workCallback)
Let a callback be run to avoid wasting time on special blocking calls.
makeKeyInternal( $keyspace, $components)
merge( $key, callable $callback, $exptime=0, $attempts=10, $flags=0)
Merge changes into the existing cache value (possibly creating a new one)
unlock( $key)
Release an advisory lock on a key string.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.