3 # Copyright (C) 2003-2004 Brion Vibber <brion@pobox.com>
4 # http://www.mediawiki.org/
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.
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.
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
25 * Simple generic object store
27 * interface is intended to be more or less compatible with
28 * the PHP memcached client.
30 * backends for local hash array and SQL table included:
31 * $bag = new HashBagOStuff();
32 * $bag = new MysqlBagOStuff($tablename); # connect to db first
38 function __construct() {
39 $this->set_debug( false );
42 function set_debug($bool) {
43 $this->debugmode
= $bool;
46 /* *** THE GUTS OF THE OPERATION *** */
47 /* Override these with functional things in subclasses */
54 function set($key, $value, $exptime=0) {
59 function delete($key, $time=0) {
64 function lock($key, $timeout = 0) {
69 function unlock($key) {
74 /* *** Emulated functions *** */
75 /* Better performance can likely be got with custom written versions */
76 function get_multi($keys) {
78 foreach($keys as $key)
79 $out[$key] = $this->get($key);
83 function set_multi($hash, $exptime=0) {
84 foreach($hash as $key => $value)
85 $this->set($key, $value, $exptime);
88 function add($key, $value, $exptime=0) {
89 if( $this->get($key) == false ) {
90 $this->set($key, $value, $exptime);
95 function add_multi($hash, $exptime=0) {
96 foreach($hash as $key => $value)
97 $this->add($key, $value, $exptime);
100 function delete_multi($keys, $time=0) {
101 foreach($keys as $key)
102 $this->delete($key, $time);
105 function replace($key, $value, $exptime=0) {
106 if( $this->get($key) !== false )
107 $this->set($key, $value, $exptime);
110 function incr($key, $value=1) {
111 if ( !$this->lock($key) ) {
114 $value = intval($value);
115 if($value < 0) $value = 0;
118 if( ($n = $this->get($key)) !== false ) {
120 $this->set($key, $n); // exptime?
126 function decr($key, $value=1) {
127 if ( !$this->lock($key) ) {
130 $value = intval($value);
131 if($value < 0) $value = 0;
134 if( ($n = $this->get($key)) !== false ) {
137 $this->set($key, $m); // exptime?
143 function _debug($text) {
145 wfDebug("BagOStuff debug: $text\n");
149 * Convert an optionally relative time to an absolute time
151 static function convertExpiry( $exptime ) {
152 if(($exptime != 0) && ($exptime < 3600*24*30)) {
153 return time() +
$exptime;
162 * Functional versions!
165 class HashBagOStuff
extends BagOStuff
{
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.
173 function HashBagOStuff() {
174 $this->bag
= array();
177 function _expire($key) {
178 $et = $this->bag
[$key][1];
179 if(($et == 0) ||
($et > time()))
186 if(!$this->bag
[$key])
188 if($this->_expire($key))
190 return $this->bag
[$key][0];
193 function set($key,$value,$exptime=0) {
194 $this->bag
[$key] = array( $value, BagOStuff
::convertExpiry( $exptime ) );
197 function delete($key,$time=0) {
198 if(!$this->bag
[$key])
200 unset($this->bag
[$key]);
206 CREATE TABLE objectcache (
207 keyname char(255) binary not null default '',
210 unique key (keyname),
219 abstract class SqlBagOStuff
extends BagOStuff
{
221 var $lastexpireall = 0;
223 function SqlBagOStuff($tablename = 'objectcache') {
224 $this->table
= $tablename;
228 /* expire old entries if any */
229 $this->garbageCollect();
231 $res = $this->_query(
232 "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key);
234 $this->_debug("get: ** error: " . $this->_dberror($res) . " **");
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() )
242 $this->_debug("get: key has expired, deleting");
246 return $this->_unserialize($this->_blobdecode($row->value
));
248 $this->_debug('get: no matching rows');
253 function set($key,$value,$exptime=0) {
254 $exptime = intval($exptime);
255 if($exptime < 0) $exptime = 0;
257 $exp = $this->_maxdatetime();
259 if($exptime < 3.16e8
) # ~10 years
261 $exp = $this->_fromunixtime($exptime);
263 $this->delete( $key );
264 $this->_doinsert($this->getTableName(), array(
266 'value' => $this->_blobencode($this->_serialize($value)),
272 function delete($key,$time=0) {
274 "DELETE FROM $0 WHERE keyname='$1'", $key );
278 function getTableName() {
282 function _query($sql) {
283 $reps = func_get_args();
284 $reps[0] = $this->getTableName();
286 for($i=0;$i<count($reps);$i++
) {
289 $i > 0 ?
$this->_strencode($reps[$i]) : $reps[$i],
292 $res = $this->_doquery($sql);
294 $this->_debug('query failed: ' . $this->_dberror($res));
299 function _strencode($str) {
300 /* Protect strings in SQL */
301 return str_replace( "'", "''", $str );
303 function _blobencode($str) {
306 function _blobdecode($str) {
310 abstract function _doinsert($table, $vals);
311 abstract function _doquery($sql);
313 function _freeresult($result) {
318 function _dberror($result) {
320 return 'unknown error';
323 abstract function _maxdatetime();
324 abstract function _fromunixtime($ts);
326 function garbageCollect() {
327 /* Ignore 99% of requests */
328 if ( !mt_rand( 0, 100 ) ) {
330 /* Avoid repeating the delete within a few seconds */
331 if ( $nowtime > ($this->lastexpireall +
1) ) {
332 $this->lastexpireall
= $nowtime;
338 function expireall() {
339 /* Remove any items that have expired */
340 $now = $this->_fromunixtime( time() );
341 $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" );
344 function deleteall() {
345 /* Clear *all* items from cache table */
346 $this->_query( "DELETE FROM $0" );
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.
357 function _serialize( &$data ) {
358 $serial = serialize( $data );
359 if( function_exists( 'gzdeflate' ) ) {
360 return gzdeflate( $serial );
367 * Unserialize and, if necessary, decompress an object.
368 * @param string $serial
371 function _unserialize( $serial ) {
372 if( function_exists( 'gzinflate' ) ) {
373 $decomp = @gzinflate
( $serial );
374 if( false !== $decomp ) {
378 $ret = unserialize( $serial );
386 class MediaWikiBagOStuff
extends SqlBagOStuff
{
387 var $tableInitialised = false;
389 function _doquery($sql) {
390 $dbw = wfGetDB( DB_MASTER
);
391 return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery');
393 function _doinsert($t, $v) {
394 $dbw = wfGetDB( DB_MASTER
);
395 return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert',
398 function _fetchobject($result) {
399 $dbw = wfGetDB( DB_MASTER
);
400 return $dbw->fetchObject($result);
402 function _freeresult($result) {
403 $dbw = wfGetDB( DB_MASTER
);
404 return $dbw->freeResult($result);
406 function _dberror($result) {
407 $dbw = wfGetDB( DB_MASTER
);
408 return $dbw->lastError();
410 function _maxdatetime() {
411 if ( time() > 0x7fffffff ) {
412 return $this->_fromunixtime( 1<<62 );
414 return $this->_fromunixtime( 0x7fffffff );
417 function _fromunixtime($ts) {
418 $dbw = wfGetDB(DB_MASTER
);
419 return $dbw->timestamp($ts);
421 function _strencode($s) {
422 $dbw = wfGetDB( DB_MASTER
);
423 return $dbw->strencode($s);
425 function _blobencode($s) {
426 $dbw = wfGetDB( DB_MASTER
);
427 return $dbw->encodeBlob($s);
429 function _blobdecode($s) {
430 $dbw = wfGetDB( DB_MASTER
);
431 return $dbw->decodeBlob($s);
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 */
439 throw new MWException("Could not connect to database");
440 $this->table
= $dbw->tableName( $this->table
);
441 $this->tableInitialised
= true;
448 * This is a wrapper for Turck MMCache's shared memory functions.
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().
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.
461 class TurckBagOStuff
extends BagOStuff
{
463 $val = mmcache_get( $key );
464 if ( is_string( $val ) ) {
465 $val = unserialize( $val );
470 function set($key, $value, $exptime=0) {
471 mmcache_put( $key, serialize( $value ), $exptime );
475 function delete($key, $time=0) {
480 function lock($key, $waitTimeout = 0 ) {
481 mmcache_lock( $key );
485 function unlock($key) {
486 mmcache_unlock( $key );
492 * This is a wrapper for APC's shared memory functions
496 class APCBagOStuff
extends BagOStuff
{
498 $val = apc_fetch($key);
499 if ( is_string( $val ) ) {
500 $val = unserialize( $val );
505 function set($key, $value, $exptime=0) {
506 apc_store($key, serialize($value), $exptime);
510 function delete($key, $time=0) {
518 * This is a wrapper for eAccelerator's shared memory functions.
520 * This is basically identical to the Turck MMCache version,
521 * mostly because eAccelerator is based on Turck MMCache.
524 class eAccelBagOStuff
extends BagOStuff
{
526 $val = eaccelerator_get( $key );
527 if ( is_string( $val ) ) {
528 $val = unserialize( $val );
533 function set($key, $value, $exptime=0) {
534 eaccelerator_put( $key, serialize( $value ), $exptime );
538 function delete($key, $time=0) {
539 eaccelerator_rm( $key );
543 function lock($key, $waitTimeout = 0 ) {
544 eaccelerator_lock( $key );
548 function unlock($key) {
549 eaccelerator_unlock( $key );
554 class DBABagOStuff
extends BagOStuff
{
555 var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
557 function __construct( $handler = 'db3', $dir = false ) {
558 if ( $dir === false ) {
559 global $wgTmpDirectory;
560 $dir = $wgTmpDirectory;
562 $this->mFile
= "$dir/mw-cache-" . wfWikiID();
563 $this->mFile
.= '.db';
564 $this->mHandler
= $handler;
568 * Encode value and expiry for storage
570 function encode( $value, $expiry ) {
571 # Convert to absolute time
572 $expiry = BagOStuff
::convertExpiry( $expiry );
573 return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value );
577 * @return list containing value first and expiry second
579 function decode( $blob ) {
580 if ( !is_string( $blob ) ) {
581 return array( null, 0 );
584 unserialize( substr( $blob, 11 ) ),
585 intval( substr( $blob, 0, 10 ) )
590 function getReader() {
591 if ( file_exists( $this->mFile
) ) {
592 $handle = dba_open( $this->mFile
, 'rl', $this->mHandler
);
594 $handle = $this->getWriter();
597 wfDebug( "Unable to open DBA cache file {$this->mFile}\n" );
602 function getWriter() {
603 $handle = dba_open( $this->mFile
, 'cl', $this->mHandler
);
605 wfDebug( "Unable to open DBA cache file {$this->mFile}\n" );
610 function get( $key ) {
611 wfProfileIn( __METHOD__
);
612 wfDebug( __METHOD__
."($key)\n" );
613 $handle = $this->getReader();
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 );
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" );
630 wfProfileOut( __METHOD__
);
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();
642 $ret = dba_replace( $key, $blob, $handle );
643 dba_close( $handle );
644 wfProfileOut( __METHOD__
);
648 function delete( $key, $time = 0 ) {
649 wfProfileIn( __METHOD__
);
650 $handle = $this->getWriter();
654 $ret = dba_delete( $key, $handle );
655 dba_close( $handle );
656 wfProfileOut( __METHOD__
);
660 function add( $key, $value, $exptime = 0 ) {
661 wfProfileIn( __METHOD__
);
662 $blob = $this->encode( $value, $exptime );
663 $handle = $this->getWriter();
667 $ret = dba_insert( $key, $blob, $handle );
668 # Insert failed, check to see if it failed due to an expired key
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
679 dba_close( $handle );
680 wfProfileOut( __METHOD__
);