Updated calls to Linker to call them statically and removed useless parameter to...
[lhc/web/wiklou.git] / includes / objectcache / SqlBagOStuff.php
1 <?php
2
3 /**
4 * Class to store objects in the database
5 *
6 * @ingroup Cache
7 */
8 class SqlBagOStuff extends BagOStuff {
9
10 /**
11 * @var LoadBalancer
12 */
13 var $lb;
14
15 /**
16 * @var DatabaseBase
17 */
18 var $db;
19 var $serverInfo;
20 var $lastExpireAll = 0;
21 var $purgePeriod = 100;
22
23 /**
24 * Constructor. Parameters are:
25 * - server: A server info structure in the format required by each
26 * element in $wgDBServers.
27 *
28 * - purgePeriod: The average number of object cache requests in between
29 * garbage collection operations, where expired entries
30 * are removed from the database. Or in other words, the
31 * reciprocal of the probability of purging on any given
32 * request. If this is set to zero, purging will never be
33 * done.
34 *
35 * @param $params array
36 */
37 public function __construct( $params ) {
38 if ( isset( $params['server'] ) ) {
39 $this->serverInfo = $params['server'];
40 $this->serverInfo['load'] = 1;
41 }
42 if ( isset( $params['purgePeriod'] ) ) {
43 $this->purgePeriod = intval( $params['purgePeriod'] );
44 }
45 }
46
47 /**
48 * @return DatabaseBase
49 */
50 protected function getDB() {
51 if ( !isset( $this->db ) ) {
52 # If server connection info was given, use that
53 if ( $this->serverInfo ) {
54 $this->lb = new LoadBalancer( array(
55 'servers' => array( $this->serverInfo ) ) );
56 $this->db = $this->lb->getConnection( DB_MASTER );
57 $this->db->clearFlag( DBO_TRX );
58 } else {
59 # We must keep a separate connection to MySQL in order to avoid deadlocks
60 # However, SQLite has an opposite behaviour.
61 # @todo Investigate behaviour for other databases
62 if ( wfGetDB( DB_MASTER )->getType() == 'sqlite' ) {
63 $this->db = wfGetDB( DB_MASTER );
64 } else {
65 $this->lb = wfGetLBFactory()->newMainLB();
66 $this->db = $this->lb->getConnection( DB_MASTER );
67 $this->db->clearFlag( DBO_TRX );
68 }
69 }
70 }
71
72 return $this->db;
73 }
74
75 public function get( $key ) {
76 # expire old entries if any
77 $this->garbageCollect();
78 $db = $this->getDB();
79 $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
80 array( 'keyname' => $key ), __METHOD__ );
81
82 if ( !$row ) {
83 $this->debug( 'get: no matching rows' );
84 return false;
85 }
86
87 $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
88
89 if ( $this->isExpired( $row->exptime ) ) {
90 $this->debug( "get: key has expired, deleting" );
91 try {
92 $db->begin();
93 # Put the expiry time in the WHERE condition to avoid deleting a
94 # newly-inserted value
95 $db->delete( 'objectcache',
96 array(
97 'keyname' => $key,
98 'exptime' => $row->exptime
99 ), __METHOD__ );
100 $db->commit();
101 } catch ( DBQueryError $e ) {
102 $this->handleWriteError( $e );
103 }
104
105 return false;
106 }
107
108 return $this->unserialize( $db->decodeBlob( $row->value ) );
109 }
110
111 public function set( $key, $value, $exptime = 0 ) {
112 $db = $this->getDB();
113 $exptime = intval( $exptime );
114
115 if ( $exptime < 0 ) {
116 $exptime = 0;
117 }
118
119 if ( $exptime == 0 ) {
120 $encExpiry = $this->getMaxDateTime();
121 } else {
122 if ( $exptime < 3.16e8 ) { # ~10 years
123 $exptime += time();
124 }
125
126 $encExpiry = $db->timestamp( $exptime );
127 }
128 try {
129 $db->begin();
130 // (bug 24425) use a replace if the db supports it instead of
131 // delete/insert to avoid clashes with conflicting keynames
132 $db->replace( 'objectcache', array( 'keyname' ),
133 array(
134 'keyname' => $key,
135 'value' => $db->encodeBlob( $this->serialize( $value ) ),
136 'exptime' => $encExpiry
137 ), __METHOD__ );
138 $db->commit();
139 } catch ( DBQueryError $e ) {
140 $this->handleWriteError( $e );
141
142 return false;
143 }
144
145 return true;
146 }
147
148 public function delete( $key, $time = 0 ) {
149 $db = $this->getDB();
150
151 try {
152 $db->begin();
153 $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
154 $db->commit();
155 } catch ( DBQueryError $e ) {
156 $this->handleWriteError( $e );
157
158 return false;
159 }
160
161 return true;
162 }
163
164 public function incr( $key, $step = 1 ) {
165 $db = $this->getDB();
166 $step = intval( $step );
167
168 try {
169 $db->begin();
170 $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
171 array( 'keyname' => $key ), __METHOD__, array( 'FOR UPDATE' ) );
172 if ( $row === false ) {
173 // Missing
174 $db->commit();
175
176 return null;
177 }
178 $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
179 if ( $this->isExpired( $row->exptime ) ) {
180 // Expired, do not reinsert
181 $db->commit();
182
183 return null;
184 }
185
186 $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
187 $newValue = $oldValue + $step;
188 $db->insert( 'objectcache',
189 array(
190 'keyname' => $key,
191 'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
192 'exptime' => $row->exptime
193 ), __METHOD__, 'IGNORE' );
194
195 if ( $db->affectedRows() == 0 ) {
196 // Race condition. See bug 28611
197 $newValue = null;
198 }
199 $db->commit();
200 } catch ( DBQueryError $e ) {
201 $this->handleWriteError( $e );
202
203 return null;
204 }
205
206 return $newValue;
207 }
208
209 public function keys() {
210 $db = $this->getDB();
211 $res = $db->select( 'objectcache', array( 'keyname' ), false, __METHOD__ );
212 $result = array();
213
214 foreach ( $res as $row ) {
215 $result[] = $row->keyname;
216 }
217
218 return $result;
219 }
220
221 protected function isExpired( $exptime ) {
222 return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time();
223 }
224
225 protected function getMaxDateTime() {
226 if ( time() > 0x7fffffff ) {
227 return $this->getDB()->timestamp( 1 << 62 );
228 } else {
229 return $this->getDB()->timestamp( 0x7fffffff );
230 }
231 }
232
233 protected function garbageCollect() {
234 if ( !$this->purgePeriod ) {
235 // Disabled
236 return;
237 }
238 // Only purge on one in every $this->purgePeriod requests.
239 if ( $this->purgePeriod !== 1 && mt_rand( 0, $this->purgePeriod - 1 ) ) {
240 return;
241 }
242 $now = time();
243 // Avoid repeating the delete within a few seconds
244 if ( $now > ( $this->lastExpireAll + 1 ) ) {
245 $this->lastExpireAll = $now;
246 $this->expireAll();
247 }
248 }
249
250 public function expireAll() {
251 $db = $this->getDB();
252 $now = $db->timestamp();
253
254 try {
255 $db->begin();
256 $db->delete( 'objectcache', array( 'exptime < ' . $db->addQuotes( $now ) ), __METHOD__ );
257 $db->commit();
258 } catch ( DBQueryError $e ) {
259 $this->handleWriteError( $e );
260 }
261 }
262
263 public function deleteAll() {
264 $db = $this->getDB();
265
266 try {
267 $db->begin();
268 $db->delete( 'objectcache', '*', __METHOD__ );
269 $db->commit();
270 } catch ( DBQueryError $e ) {
271 $this->handleWriteError( $e );
272 }
273 }
274
275 /**
276 * Serialize an object and, if possible, compress the representation.
277 * On typical message and page data, this can provide a 3X decrease
278 * in storage requirements.
279 *
280 * @param $data mixed
281 * @return string
282 */
283 protected function serialize( &$data ) {
284 $serial = serialize( $data );
285
286 if ( function_exists( 'gzdeflate' ) ) {
287 return gzdeflate( $serial );
288 } else {
289 return $serial;
290 }
291 }
292
293 /**
294 * Unserialize and, if necessary, decompress an object.
295 * @param $serial string
296 * @return mixed
297 */
298 protected function unserialize( $serial ) {
299 if ( function_exists( 'gzinflate' ) ) {
300 $decomp = @gzinflate( $serial );
301
302 if ( false !== $decomp ) {
303 $serial = $decomp;
304 }
305 }
306
307 $ret = unserialize( $serial );
308
309 return $ret;
310 }
311
312 /**
313 * Handle a DBQueryError which occurred during a write operation.
314 * Ignore errors which are due to a read-only database, rethrow others.
315 */
316 protected function handleWriteError( $exception ) {
317 $db = $this->getDB();
318
319 if ( !$db->wasReadOnlyError() ) {
320 throw $exception;
321 }
322
323 try {
324 $db->rollback();
325 } catch ( DBQueryError $e ) {
326 }
327
328 wfDebug( __METHOD__ . ": ignoring query error\n" );
329 $db->ignoreErrors( false );
330 }
331 }
332
333 /**
334 * Backwards compatibility alias
335 */
336 class MediaWikiBagOStuff extends SqlBagOStuff { }
337