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_NONKEY,
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 incrWithInit( $key, $exptime, $step = 1, $init = null, $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 setMockTime( &$time ) {
263  parent::setMockTime( $time );
264  $this->writeStore->setMockTime( $time );
265  $this->readStore->setMockTime( $time );
266  }
267 
272  private function hadRecentSessionWrite( array $keys ) {
273  $now = $this->getCurrentTime();
274  foreach ( $keys as $key ) {
275  $ts = $this->lastKeyWrites[$key] ?? 0;
276  if ( $ts && ( $now - $ts ) <= $this->consistencyWindow ) {
277  return true;
278  }
279  }
280 
281  return false;
282  }
283 
287  private function remarkRecentSessionWrite( array $keys ) {
288  $now = $this->getCurrentTime();
289  foreach ( $keys as $key ) {
290  // move to the end
291  unset( $this->lastKeyWrites[$key] );
292  $this->lastKeyWrites[$key] = $now;
293  }
294  // Prune out the map if the first key is obsolete
295  if ( ( $now - reset( $this->lastKeyWrites ) ) > $this->consistencyWindow ) {
296  $this->lastKeyWrites = array_filter(
297  $this->lastKeyWrites,
298  function ( $timestamp ) use ( $now ) {
299  return ( ( $now - $timestamp ) <= $this->consistencyWindow );
300  }
301  );
302  }
303  }
304 }
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:601
fieldHasFlags( $field, $flags)
Definition: BagOStuff.php:591
getCurrentTime()
Definition: BagOStuff.php:802
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.
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.
setMulti(array $valueByKey, $exptime=0, $flags=0)
Set a batch of items.
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.