The war on redundant ampersand usage!
[lhc/web/wiklou.git] / includes / BagOStuff.php
1 <?php
2 #
3 # Copyright (C) 2003-2004 Brion Vibber <brion@pobox.com>
4 # http://www.mediawiki.org/
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # http://www.gnu.org/copyleft/gpl.html
20 /**
21 *
22 */
23
24 /**
25 * Simple generic object store
26 *
27 * interface is intended to be more or less compatible with
28 * the PHP memcached client.
29 *
30 * backends for local hash array and SQL table included:
31 * $bag = new HashBagOStuff();
32 * $bag = new MysqlBagOStuff($tablename); # connect to db first
33 *
34 */
35 class BagOStuff {
36 var $debugmode;
37
38 function __construct() {
39 $this->set_debug( false );
40 }
41
42 function set_debug($bool) {
43 $this->debugmode = $bool;
44 }
45
46 /* *** THE GUTS OF THE OPERATION *** */
47 /* Override these with functional things in subclasses */
48
49 function get($key) {
50 /* stub */
51 return false;
52 }
53
54 function set($key, $value, $exptime=0) {
55 /* stub */
56 return false;
57 }
58
59 function delete($key, $time=0) {
60 /* stub */
61 return false;
62 }
63
64 function lock($key, $timeout = 0) {
65 /* stub */
66 return true;
67 }
68
69 function unlock($key) {
70 /* stub */
71 return true;
72 }
73
74 /* *** Emulated functions *** */
75 /* Better performance can likely be got with custom written versions */
76 function get_multi($keys) {
77 $out = array();
78 foreach($keys as $key)
79 $out[$key] = $this->get($key);
80 return $out;
81 }
82
83 function set_multi($hash, $exptime=0) {
84 foreach($hash as $key => $value)
85 $this->set($key, $value, $exptime);
86 }
87
88 function add($key, $value, $exptime=0) {
89 if( $this->get($key) == false ) {
90 $this->set($key, $value, $exptime);
91 return true;
92 }
93 }
94
95 function add_multi($hash, $exptime=0) {
96 foreach($hash as $key => $value)
97 $this->add($key, $value, $exptime);
98 }
99
100 function delete_multi($keys, $time=0) {
101 foreach($keys as $key)
102 $this->delete($key, $time);
103 }
104
105 function replace($key, $value, $exptime=0) {
106 if( $this->get($key) !== false )
107 $this->set($key, $value, $exptime);
108 }
109
110 function incr($key, $value=1) {
111 if ( !$this->lock($key) ) {
112 return false;
113 }
114 $value = intval($value);
115 if($value < 0) $value = 0;
116
117 $n = false;
118 if( ($n = $this->get($key)) !== false ) {
119 $n += $value;
120 $this->set($key, $n); // exptime?
121 }
122 $this->unlock($key);
123 return $n;
124 }
125
126 function decr($key, $value=1) {
127 if ( !$this->lock($key) ) {
128 return false;
129 }
130 $value = intval($value);
131 if($value < 0) $value = 0;
132
133 $m = false;
134 if( ($n = $this->get($key)) !== false ) {
135 $m = $n - $value;
136 if($m < 0) $m = 0;
137 $this->set($key, $m); // exptime?
138 }
139 $this->unlock($key);
140 return $m;
141 }
142
143 function _debug($text) {
144 if($this->debugmode)
145 wfDebug("BagOStuff debug: $text\n");
146 }
147
148 /**
149 * Convert an optionally relative time to an absolute time
150 */
151 static function convertExpiry( $exptime ) {
152 if(($exptime != 0) && ($exptime < 3600*24*30)) {
153 return time() + $exptime;
154 } else {
155 return $exptime;
156 }
157 }
158 }
159
160
161 /**
162 * Functional versions!
163 * @todo document
164 */
165 class HashBagOStuff extends BagOStuff {
166 /*
167 This is a test of the interface, mainly. It stores
168 things in an associative array, which is not going to
169 persist between program runs.
170 */
171 var $bag;
172
173 function HashBagOStuff() {
174 $this->bag = array();
175 }
176
177 function _expire($key) {
178 $et = $this->bag[$key][1];
179 if(($et == 0) || ($et > time()))
180 return false;
181 $this->delete($key);
182 return true;
183 }
184
185 function get($key) {
186 if(!$this->bag[$key])
187 return false;
188 if($this->_expire($key))
189 return false;
190 return $this->bag[$key][0];
191 }
192
193 function set($key,$value,$exptime=0) {
194 $this->bag[$key] = array( $value, BagOStuff::convertExpiry( $exptime ) );
195 }
196
197 function delete($key,$time=0) {
198 if(!$this->bag[$key])
199 return false;
200 unset($this->bag[$key]);
201 return true;
202 }
203 }
204
205 /*
206 CREATE TABLE objectcache (
207 keyname char(255) binary not null default '',
208 value mediumblob,
209 exptime datetime,
210 unique key (keyname),
211 key (exptime)
212 );
213 */
214
215 /**
216 * @todo document
217 * @abstract
218 */
219 abstract class SqlBagOStuff extends BagOStuff {
220 var $table;
221 var $lastexpireall = 0;
222
223 function SqlBagOStuff($tablename = 'objectcache') {
224 $this->table = $tablename;
225 }
226
227 function get($key) {
228 /* expire old entries if any */
229 $this->garbageCollect();
230
231 $res = $this->_query(
232 "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key);
233 if(!$res) {
234 $this->_debug("get: ** error: " . $this->_dberror($res) . " **");
235 return false;
236 }
237 if($row=$this->_fetchobject($res)) {
238 $this->_debug("get: retrieved data; exp time is " . $row->exptime);
239 if ( $row->exptime != $this->_maxdatetime() &&
240 wfTimestamp( TS_UNIX, $row->exptime ) < time() )
241 {
242 $this->_debug("get: key has expired, deleting");
243 $this->delete($key);
244 return false;
245 }
246 return $this->_unserialize($this->_blobdecode($row->value));
247 } else {
248 $this->_debug('get: no matching rows');
249 }
250 return false;
251 }
252
253 function set($key,$value,$exptime=0) {
254 $exptime = intval($exptime);
255 if($exptime < 0) $exptime = 0;
256 if($exptime == 0) {
257 $exp = $this->_maxdatetime();
258 } else {
259 if($exptime < 3.16e8) # ~10 years
260 $exptime += time();
261 $exp = $this->_fromunixtime($exptime);
262 }
263 $this->delete( $key );
264 $this->_doinsert($this->getTableName(), array(
265 'keyname' => $key,
266 'value' => $this->_blobencode($this->_serialize($value)),
267 'exptime' => $exp
268 ));
269 return true; /* ? */
270 }
271
272 function delete($key,$time=0) {
273 $this->_query(
274 "DELETE FROM $0 WHERE keyname='$1'", $key );
275 return true; /* ? */
276 }
277
278 function getTableName() {
279 return $this->table;
280 }
281
282 function _query($sql) {
283 $reps = func_get_args();
284 $reps[0] = $this->getTableName();
285 // ewwww
286 for($i=0;$i<count($reps);$i++) {
287 $sql = str_replace(
288 '$' . $i,
289 $i > 0 ? $this->_strencode($reps[$i]) : $reps[$i],
290 $sql);
291 }
292 $res = $this->_doquery($sql);
293 if($res == false) {
294 $this->_debug('query failed: ' . $this->_dberror($res));
295 }
296 return $res;
297 }
298
299 function _strencode($str) {
300 /* Protect strings in SQL */
301 return str_replace( "'", "''", $str );
302 }
303 function _blobencode($str) {
304 return $str;
305 }
306 function _blobdecode($str) {
307 return $str;
308 }
309
310 abstract function _doinsert($table, $vals);
311 abstract function _doquery($sql);
312
313 function _freeresult($result) {
314 /* stub */
315 return false;
316 }
317
318 function _dberror($result) {
319 /* stub */
320 return 'unknown error';
321 }
322
323 abstract function _maxdatetime();
324 abstract function _fromunixtime($ts);
325
326 function garbageCollect() {
327 /* Ignore 99% of requests */
328 if ( !mt_rand( 0, 100 ) ) {
329 $nowtime = time();
330 /* Avoid repeating the delete within a few seconds */
331 if ( $nowtime > ($this->lastexpireall + 1) ) {
332 $this->lastexpireall = $nowtime;
333 $this->expireall();
334 }
335 }
336 }
337
338 function expireall() {
339 /* Remove any items that have expired */
340 $now = $this->_fromunixtime( time() );
341 $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" );
342 }
343
344 function deleteall() {
345 /* Clear *all* items from cache table */
346 $this->_query( "DELETE FROM $0" );
347 }
348
349 /**
350 * Serialize an object and, if possible, compress the representation.
351 * On typical message and page data, this can provide a 3X decrease
352 * in storage requirements.
353 *
354 * @param mixed $data
355 * @return string
356 */
357 function _serialize( &$data ) {
358 $serial = serialize( $data );
359 if( function_exists( 'gzdeflate' ) ) {
360 return gzdeflate( $serial );
361 } else {
362 return $serial;
363 }
364 }
365
366 /**
367 * Unserialize and, if necessary, decompress an object.
368 * @param string $serial
369 * @return mixed
370 */
371 function _unserialize( $serial ) {
372 if( function_exists( 'gzinflate' ) ) {
373 $decomp = @gzinflate( $serial );
374 if( false !== $decomp ) {
375 $serial = $decomp;
376 }
377 }
378 $ret = unserialize( $serial );
379 return $ret;
380 }
381 }
382
383 /**
384 * @todo document
385 */
386 class MediaWikiBagOStuff extends SqlBagOStuff {
387 var $tableInitialised = false;
388
389 function _doquery($sql) {
390 $dbw = wfGetDB( DB_MASTER );
391 return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery');
392 }
393 function _doinsert($t, $v) {
394 $dbw = wfGetDB( DB_MASTER );
395 return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert',
396 array( 'IGNORE' ) );
397 }
398 function _fetchobject($result) {
399 $dbw = wfGetDB( DB_MASTER );
400 return $dbw->fetchObject($result);
401 }
402 function _freeresult($result) {
403 $dbw = wfGetDB( DB_MASTER );
404 return $dbw->freeResult($result);
405 }
406 function _dberror($result) {
407 $dbw = wfGetDB( DB_MASTER );
408 return $dbw->lastError();
409 }
410 function _maxdatetime() {
411 if ( time() > 0x7fffffff ) {
412 return $this->_fromunixtime( 1<<62 );
413 } else {
414 return $this->_fromunixtime( 0x7fffffff );
415 }
416 }
417 function _fromunixtime($ts) {
418 $dbw = wfGetDB(DB_MASTER);
419 return $dbw->timestamp($ts);
420 }
421 function _strencode($s) {
422 $dbw = wfGetDB( DB_MASTER );
423 return $dbw->strencode($s);
424 }
425 function _blobencode($s) {
426 $dbw = wfGetDB( DB_MASTER );
427 return $dbw->encodeBlob($s);
428 }
429 function _blobdecode($s) {
430 $dbw = wfGetDB( DB_MASTER );
431 return $dbw->decodeBlob($s);
432 }
433 function getTableName() {
434 if ( !$this->tableInitialised ) {
435 $dbw = wfGetDB( DB_MASTER );
436 /* This is actually a hack, we should be able
437 to use Language classes here... or not */
438 if (!$dbw)
439 throw new MWException("Could not connect to database");
440 $this->table = $dbw->tableName( $this->table );
441 $this->tableInitialised = true;
442 }
443 return $this->table;
444 }
445 }
446
447 /**
448 * This is a wrapper for Turck MMCache's shared memory functions.
449 *
450 * You can store objects with mmcache_put() and mmcache_get(), but Turck seems
451 * to use a weird custom serializer that randomly segfaults. So we wrap calls
452 * with serialize()/unserialize().
453 *
454 * The thing I noticed about the Turck serialized data was that unlike ordinary
455 * serialize(), it contained the names of methods, and judging by the amount of
456 * binary data, perhaps even the bytecode of the methods themselves. It may be
457 * that Turck's serializer is faster, so a possible future extension would be
458 * to use it for arrays but not for objects.
459 *
460 */
461 class TurckBagOStuff extends BagOStuff {
462 function get($key) {
463 $val = mmcache_get( $key );
464 if ( is_string( $val ) ) {
465 $val = unserialize( $val );
466 }
467 return $val;
468 }
469
470 function set($key, $value, $exptime=0) {
471 mmcache_put( $key, serialize( $value ), $exptime );
472 return true;
473 }
474
475 function delete($key, $time=0) {
476 mmcache_rm( $key );
477 return true;
478 }
479
480 function lock($key, $waitTimeout = 0 ) {
481 mmcache_lock( $key );
482 return true;
483 }
484
485 function unlock($key) {
486 mmcache_unlock( $key );
487 return true;
488 }
489 }
490
491 /**
492 * This is a wrapper for APC's shared memory functions
493 *
494 */
495
496 class APCBagOStuff extends BagOStuff {
497 function get($key) {
498 $val = apc_fetch($key);
499 if ( is_string( $val ) ) {
500 $val = unserialize( $val );
501 }
502 return $val;
503 }
504
505 function set($key, $value, $exptime=0) {
506 apc_store($key, serialize($value), $exptime);
507 return true;
508 }
509
510 function delete($key, $time=0) {
511 apc_delete($key);
512 return true;
513 }
514 }
515
516
517 /**
518 * This is a wrapper for eAccelerator's shared memory functions.
519 *
520 * This is basically identical to the Turck MMCache version,
521 * mostly because eAccelerator is based on Turck MMCache.
522 *
523 */
524 class eAccelBagOStuff extends BagOStuff {
525 function get($key) {
526 $val = eaccelerator_get( $key );
527 if ( is_string( $val ) ) {
528 $val = unserialize( $val );
529 }
530 return $val;
531 }
532
533 function set($key, $value, $exptime=0) {
534 eaccelerator_put( $key, serialize( $value ), $exptime );
535 return true;
536 }
537
538 function delete($key, $time=0) {
539 eaccelerator_rm( $key );
540 return true;
541 }
542
543 function lock($key, $waitTimeout = 0 ) {
544 eaccelerator_lock( $key );
545 return true;
546 }
547
548 function unlock($key) {
549 eaccelerator_unlock( $key );
550 return true;
551 }
552 }
553
554 class DBABagOStuff extends BagOStuff {
555 var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
556
557 function __construct( $handler = 'db3', $dir = false ) {
558 if ( $dir === false ) {
559 global $wgTmpDirectory;
560 $dir = $wgTmpDirectory;
561 }
562 $this->mFile = "$dir/mw-cache-" . wfWikiID();
563 $this->mFile .= '.db';
564 $this->mHandler = $handler;
565 }
566
567 /**
568 * Encode value and expiry for storage
569 */
570 function encode( $value, $expiry ) {
571 # Convert to absolute time
572 $expiry = BagOStuff::convertExpiry( $expiry );
573 return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value );
574 }
575
576 /**
577 * @return list containing value first and expiry second
578 */
579 function decode( $blob ) {
580 if ( !is_string( $blob ) ) {
581 return array( null, 0 );
582 } else {
583 return array(
584 unserialize( substr( $blob, 11 ) ),
585 intval( substr( $blob, 0, 10 ) )
586 );
587 }
588 }
589
590 function getReader() {
591 if ( file_exists( $this->mFile ) ) {
592 $handle = dba_open( $this->mFile, 'rl', $this->mHandler );
593 } else {
594 $handle = $this->getWriter();
595 }
596 if ( !$handle ) {
597 wfDebug( "Unable to open DBA cache file {$this->mFile}\n" );
598 }
599 return $handle;
600 }
601
602 function getWriter() {
603 $handle = dba_open( $this->mFile, 'cl', $this->mHandler );
604 if ( !$handle ) {
605 wfDebug( "Unable to open DBA cache file {$this->mFile}\n" );
606 }
607 return $handle;
608 }
609
610 function get( $key ) {
611 wfProfileIn( __METHOD__ );
612 wfDebug( __METHOD__."($key)\n" );
613 $handle = $this->getReader();
614 if ( !$handle ) {
615 return null;
616 }
617 $val = dba_fetch( $key, $handle );
618 list( $val, $expiry ) = $this->decode( $val );
619 # Must close ASAP because locks are held
620 dba_close( $handle );
621
622 if ( !is_null( $val ) && $expiry && $expiry < time() ) {
623 # Key is expired, delete it
624 $handle = $this->getWriter();
625 dba_delete( $key, $handle );
626 dba_close( $handle );
627 wfDebug( __METHOD__.": $key expired\n" );
628 $val = null;
629 }
630 wfProfileOut( __METHOD__ );
631 return $val;
632 }
633
634 function set( $key, $value, $exptime=0 ) {
635 wfProfileIn( __METHOD__ );
636 wfDebug( __METHOD__."($key)\n" );
637 $blob = $this->encode( $value, $exptime );
638 $handle = $this->getWriter();
639 if ( !$handle ) {
640 return false;
641 }
642 $ret = dba_replace( $key, $blob, $handle );
643 dba_close( $handle );
644 wfProfileOut( __METHOD__ );
645 return $ret;
646 }
647
648 function delete( $key, $time = 0 ) {
649 wfProfileIn( __METHOD__ );
650 $handle = $this->getWriter();
651 if ( !$handle ) {
652 return false;
653 }
654 $ret = dba_delete( $key, $handle );
655 dba_close( $handle );
656 wfProfileOut( __METHOD__ );
657 return $ret;
658 }
659
660 function add( $key, $value, $exptime = 0 ) {
661 wfProfileIn( __METHOD__ );
662 $blob = $this->encode( $value, $exptime );
663 $handle = $this->getWriter();
664 if ( !$handle ) {
665 return false;
666 }
667 $ret = dba_insert( $key, $blob, $handle );
668 # Insert failed, check to see if it failed due to an expired key
669 if ( !$ret ) {
670 list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) );
671 if ( $expiry < time() ) {
672 # Yes expired, delete and try again
673 dba_delete( $key, $handle );
674 $ret = dba_insert( $key, $blob, $handle );
675 # This time if it failed then it will be handled by the caller like any other race
676 }
677 }
678
679 dba_close( $handle );
680 wfProfileOut( __METHOD__ );
681 return $ret;
682 }
683 }
684
685 ?>