From fd305c509f255aada11bde3580873bd6eaac6f76 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 11 Aug 2003 13:53:20 +0000 Subject: [PATCH] Initial support for memcached. Includes PHP MemCachedClient 1.0.8 by Ryan Gilfether, under GPL license. --- docs/memcached.doc | 105 +++ docs/php-memcached/ChangeLog | 36 + docs/php-memcached/Documentation | 257 +++++++ includes/DefaultSettings.php | 7 + includes/LinkCache.php | 25 +- includes/MemCachedClient.inc.php | 1104 ++++++++++++++++++++++++++++++ includes/Setup.php | 19 +- includes/User.php | 51 +- includes/UserTalkUpdate.php | 6 +- 9 files changed, 1576 insertions(+), 34 deletions(-) create mode 100644 docs/memcached.doc create mode 100644 docs/php-memcached/ChangeLog create mode 100644 docs/php-memcached/Documentation create mode 100644 includes/MemCachedClient.inc.php diff --git a/docs/memcached.doc b/docs/memcached.doc new file mode 100644 index 0000000000..c79865d6bb --- /dev/null +++ b/docs/memcached.doc @@ -0,0 +1,105 @@ +memcached support for MediaWiki: + +From ca August 2003, MediaWiki has optional support for memcached, a +"high-performance, distributed memory object caching system". +For general information on it, see: http://www.danga.com/memcached/ + +Memcached is likely more trouble than a small site will need, but +for a larger site with heavy load, like Wikipedia, it should help +lighten the load on the database servers by caching data and objects +in memory. + +== Requirements == + +* PHP must be compiled with --enable-sockets + +* libevent: http://www.monkey.org/~provos/libevent/ + (as of 2003-08-11, 0.7a is current) + +* optionally, epoll-rt patch for Linux kernel: + http://www.xmailserver.org/linux-patches/nio-improve.html + +* memcached: http://www.danga.com/memcached/download.bml + (as of this writing, 1.1.9 is current) + +Memcached and libevent are under BSD-style licenses. + +The server should run on Linux and other Unix-like systems... you +can run multiple servers on one machine or on multiple machines on +a network; storage can be distributed across multiple servers, and +multiple web servers can use the same cache cluster. + + +********************* W A R N I N G ! ! ! ! ! *********************** +Memcached has no security or authentication. Please ensure that your +server is appropriately firewalled, and that the port(s) used for +memcached servers are not publicly accessible. Otherwise, anyone on +the internet can put data into and read data from your cache. + +An attacker familiar with MediaWiki internals could use this to give +themselves developer access and delete all data from the wiki's +database, as well as getting all users' password hashes and e-mail +addresses. +********************* W A R N I N G ! ! ! ! ! *********************** + +== Setup == + +If you want to start small, just run one memcached on your web +server: + + memcached -d -l 127.0.0.1 -p 11000 -m 64 + +(to run in daemon mode, accessible only via loopback interface, +on port 11000, using up to 64MB of memory) + +In your LocalSettings.php file, set: + + $wgUseMemCached = true; + $wgMemCachedServers = array( "127.0.0.1:11000" ); + +The wiki should then use memcached to cache various data. To use +multiple servers (physically separate boxes or multiple caches +on one machine on a large-memory x86 box), just add more items +to the array. To increase the weight of a server (say, because +it has twice the memory of the others and you want to spread +usage evenly), make its entry a subarray: + + $wgMemCachedServers = array( + "127.0.0.1:11000", # one gig on this box + array("192.168.0.1:11000", 2) # two gigs on the other box + ); + + +== PHP client for memcached == + +As of this writing, MediaWiki includes version 1.0.8 of the PHP +memcached client by Ryan Gilfether . +You'll find some documentation for it in the 'php-memcached' +subdirectory under the present one. + +We intend to track updates, but if you want to check for the lastest +released version, see http://www.danga.com/memcached/apis.bml + +If you don't set $wgUseMemCached, we still create a MemCacheClient, +but requests to it are no-ops and we always fall through to the +database. If the cache daemon can't be contacted, it should also +disable itself fairly smoothly. + +== Keys used == + +User: + key: $wgDBname:user:user_id:$sId + ex: wikidb:user:user_id:51 + stores: instance of class User + set in: User::loadFromSession() + cleared by: User::saveSettings() + +LinkCache: + key: $wgDBname:linkcache:title:$title + ex: wikidb:linkcache:title:Wikipedia:Welcome,_Newcomers! + stores: cur_id of page, or 0 if page does not exist + set in: LinkCache::addLink() + cleared by: LinkCache::clearBadLink() + should be cleared on page deletion and rename + +... more to come ... diff --git a/docs/php-memcached/ChangeLog b/docs/php-memcached/ChangeLog new file mode 100644 index 0000000000..bfabb494f3 --- /dev/null +++ b/docs/php-memcached/ChangeLog @@ -0,0 +1,36 @@ +Release 1.0.8 +------------- +* whitespace/punctuation/wording cleanups + +Release 1.0.7 +------------- +* added 3 functions which handle error reporting + error() - returns error number of last error generated, else returns 0 + error_string() - returns a string description of error number retuned + error_clear() - clears the last error number and error string +* removed call to preg_match() in _loaditems() +* only non-scalar values are serialize() before being + sent to the server +* added the optional timestamp argument for delete() + read Documentation file for details +* PHPDocs/PEAR style comments added +* abstract debugging (Brion Vibber ) + +Release 1.0.6 +------------- +* removed all array_push() calls +* applied patch provided by Stuart Herbert + corrects possible endless loop. Available at + http://bugs.gentoo.org/show_bug.cgi?id=25385 +* fixed problem with storing large binary files +* added more error checking, specifically on all socket functions +* added support for the INCR and DECR commands + which increment or decrement a value stored in MemCached +* Documentation removed from source and is now available + in the file Documentation + +Release 1.0.4 +------------- +* initial release, version numbers kept + in sync with MemCached version +* capable of storing any datatype in MemCached diff --git a/docs/php-memcached/Documentation b/docs/php-memcached/Documentation new file mode 100644 index 0000000000..0d09d17860 --- /dev/null +++ b/docs/php-memcached/Documentation @@ -0,0 +1,257 @@ +Ryan Gilfether +http://www.gilfether.com +This module is Copyright (c) 2003 Ryan Gilfether. +All rights reserved. + +You may distribute under the terms of the GNU General Public License +This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND. + +See the memcached website: http://www.danga.com/memcached/ + + +// Takes one parameter, a array of options. The most important key is +// options["servers"], but that can also be set later with the set_servers() +// method. The servers must be an array of hosts, each of which is +// either a scalar of the form <10.0.0.10:11211> or an array of the +// former and an integer weight value. (the default weight if +// unspecified is 1.) It's recommended that weight values be kept as low +// as possible, as this module currently allocates memory for bucket +// distribution proportional to the total host weights. +// $options["debug"] turns the debugging on if set to true +MemCachedClient::MemCachedClient($options); + +// sets up the list of servers and the ports to connect to +// takes an array of servers in the same format as in the constructor +MemCachedClient::set_servers($servers); + +// Retrieves a key from the memcache. Returns the value (automatically +// unserialized, if necessary) or FALSE if it fails. +// The $key can optionally be an array, with the first element being the +// hash value, if you want to avoid making this module calculate a hash +// value. You may prefer, for example, to keep all of a given user's +// objects on the same memcache server, so you could use the user's +// unique id as the hash value. +// Possible errors set are: +// MC_ERR_GET +MemCachedClient::get($key); + +// just like get(), but takes an array of keys, returns FALSE on error +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +MemCachedClient::get_multi($keys) + +// Unconditionally sets a key to a given value in the memcache. Returns true +// if it was stored successfully. +// The $key can optionally be an arrayref, with the first element being the +// hash value, as described above. +// returns TRUE on success else FALSE +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// MC_ERR_SET +MemCachedClient::set($key, $value, $exptime); + +// Like set(), but only stores in memcache if the key doesn't already exist. +// returns TRUE on success else FALSE +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// MC_ERR_SET +MemCachedClient::add($key, $value, $exptime); + +// Like set(), but only stores in memcache if the key already exists. +// returns TRUE on success else FALSE +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// MC_ERR_SET +MemCachedClient::replace($key, $value, $exptime); + +// removes the key from the MemCache +// $time is the amount of time in seconds (or Unix time) until which +// the client wishes the server to refuse "add" and "replace" commands +// with this key. For this amount of item, the item is put into a +// delete queue, which means that it won't possible to retrieve it by +// the "get" command, but "add" and "replace" command with this key +// will also fail (the "set" command will succeed, however). After the +// time passes, the item is finally deleted from server memory. +// The parameter $time is optional, and, if absent, defaults to 0 +// (which means that the item will be deleted immediately and further +// storage commands with this key will succeed). +// returns TRUE on success else returns FALSE +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// MC_ERR_DELETE +MemCachedClient::delete($key, $time = 0); + +// Sends a command to the server to atomically increment the value for +// $key by $value, or by 1 if $value is undefined. Returns FALSE if $key +// doesn't exist on server, otherwise it returns the new value after +// incrementing. Value should be zero or greater. Overflow on server +// is not checked. Be aware of values approaching 2**32. See decr. +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// returns new value on success, else returns FALSE +// ONLY WORKS WITH NUMERIC VALUES +MemCachedClient::incr($key[, $value]); + +// Like incr, but decrements. Unlike incr, underflow is checked and new +// values are capped at 0. If server value is 1, a decrement of 2 +// returns 0, not -1. +// Possible errors set are: +// MC_ERR_NOT_ACTIVE +// MC_ERR_GET_SOCK +// MC_ERR_SOCKET_WRITE +// MC_ERR_SOCKET_READ +// returns new value on success, else returns FALSE +// ONLY WORKS WITH NUMERIC VALUES +MemCachedClient::decr($key[, $value]); + +// disconnects from all servers +MemCachedClient::disconnect_all(); + +// if $do_debug is set to true, will print out +// debugging info, else debug is turned off +MemCachedClient::set_debug($do_debug); + +// remove all cached hosts that are no longer good +MemCachedClient::forget_dead_hosts(); + +// When a function returns FALSE, an error code is set. +// This funtion will return the error code. +// See error_string() +// returns last error code set +MemCachedClient::error() + +// Returns a string describing the error set in error() +// See error() +// returns a string describing the error code given +MemCachedClient::error_string() + +// Resets the error number and error string +MemCachedClient::error_clear() + +Error codes are as follows: +MC_ERR_NOT_ACTIVE // no active servers +MC_ERR_SOCKET_WRITE // socket_write() failed +MC_ERR_SOCKET_READ // socket_read() failed +MC_ERR_SOCKET_CONNECT // failed to connect to host +MC_ERR_DELETE // delete() did not recieve DELETED command +MC_ERR_HOST_FORMAT // sock_to_host() invalid host format +MC_ERR_HOST_DEAD // sock_to_host() host is dead +MC_ERR_GET_SOCK // get_sock() failed to find a valid socket +MC_ERR_SET // _set() failed to receive the STORED response +MC_ERR_LOADITEM_HEADER // _load_items failed to receive valid data header +MC_ERR_LOADITEM_END // _load_items failed to receive END response +MC_ERR_LOADITEM_BYTES // _load_items bytes read larger than bytes available +MC_ERR_GET // failed to get value associated with key + + + +EXAMPLE: +set("key_one", $myarr); +$val = $memc->get("key_one"); +print $val[0]."\n"; // prints 'one' +print $val[1]."\n"; // prints 'two' +print $val[2]."\n"; // prints 3 + + +print "\n"; + + +/*********************** + * STORE A CLASS + ***********************/ +class tester +{ + var $one; + var $two; + var $three; +} + +$t = new tester; +$t->one = "one"; +$t->two = "two"; +$t->three = 3; +$memc->set("key_two", $t); +$val = $memc->get("key_two"); +print $val->one."\n"; +print $val->two."\n"; +print $val->three."\n"; + + +print "\n"; + + +/*********************** + * STORE A STRING + ***********************/ +$memc->set("key_three", "my string"); +$val = $memc->get("key_three"); +print $val; // prints 'my string' + +$memc->delete("key_one"); +$memc->delete("key_two"); +$memc->delete("key_three"); + +$memc->disconnect_all(); + + + +print "\n"; + + +/*********************** + * STORE A BINARY FILE + ***********************/ + + // first read the file and save it in memcache +$fp = fopen( "./image.jpg", "rb" ) ; +if ( !$fp ) +{ + print "Could not open ./file.dat!\n" ; + exit ; +} +$data = fread( $fp, filesize( "./image.jpg" ) ) ; +fclose( $fp ) ; +print "Data length is " . strlen( $data ) . "\n" ; +$memc->set( "key", $data ) ; + +// now open a file for writing and write the data +// retrieved from memcache +$fp = fopen("./test.jpg","wb"); +$data = $memc->get( "key" ) ; +print "Data length is " . strlen( $data ) . "\n" ; +fwrite($fp,$data,strlen( $data )); +fclose($fp); + + +?> + + diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 98fcbde194..031c7e5b78 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -38,6 +38,13 @@ $wgDBminWordLen = 4; $wgDBtransactions = false; # Set to true if using InnoDB tables $wgDBmysql4 = false; # Set to true to use enhanced fulltext search +# memcached settings +# See docs/memcached.doc +# +$wgUseMemCached = false; +$wgMemCachedServers = array( "127.0.0.1:11000" ); +$wgMemCachedDebug = false; + # Language settings # $wgLanguageCode = "en"; diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 0955edda3a..62bf9dedb0 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -62,6 +62,8 @@ class LinkCache { if ( isset( $index ) ) { unset( $this->mBadLinks[$index] ); } + global $wgMemc, $wgDBname; + $wgMemc->delete( "$wgDBname:linkcache:title:$title" ); } function suspend() { $this->mActive = false; } @@ -76,6 +78,7 @@ class LinkCache { $id = $this->getGoodLinkID( $title ); if ( 0 != $id ) { return $id; } + global $wgMemc, $wgDBname; wfProfileIn( "LinkCache::addLink-checkdatabase" ); $nt = Title::newFromDBkey( $title ); @@ -83,15 +86,20 @@ class LinkCache { $t = $nt->getDBkey(); if ( "" == $t ) { return 0; } - $sql = "SELECT HIGH_PRIORITY cur_id FROM cur WHERE cur_namespace=" . - "{$ns} AND cur_title='" . wfStrencode( $t ) . "'"; - $res = wfQuery( $sql, "LinkCache::addLink" ); - if ( 0 == wfNumRows( $res ) ) { - $id = 0; - } else { - $s = wfFetchObject( $res ); - $id = $s->cur_id; + $id = $wgMemc->get( $key = "$wgDBname:linkcache:title:$title" ); + if( $id === FALSE ) { + $sql = "SELECT HIGH_PRIORITY cur_id FROM cur WHERE cur_namespace=" . + "{$ns} AND cur_title='" . wfStrencode( $t ) . "'"; + $res = wfQuery( $sql, "LinkCache::addLink" ); + + if ( 0 == wfNumRows( $res ) ) { + $id = 0; + } else { + $s = wfFetchObject( $res ); + $id = $s->cur_id; + } + $wgMemc->add( $key, $id, time()+3600 ); } if ( 0 == $id ) { $this->addBadLink( $title ); } else { $this->addGoodLink( $id, $title ); } @@ -200,5 +208,6 @@ class LinkCache { $this->mBadLinks = array(); $this->mImageLinks = array(); } + } ?> diff --git a/includes/MemCachedClient.inc.php b/includes/MemCachedClient.inc.php new file mode 100644 index 0000000000..b1a35a230b --- /dev/null +++ b/includes/MemCachedClient.inc.php @@ -0,0 +1,1104 @@ + + * http://www.gilfether.com + * + * Originally translated from Brad Fitzpatrick's MemCached Perl client + * See the memcached website: + * http://www.danga.com/memcached/ + * + * This module is Copyright (c) 2003 Ryan Gilfether. + * All rights reserved. + * You may distribute under the terms of the GNU General Public License + * This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND. + * + */ + +/** + * version string + */ +define("MC_VERSION", "1.0.8"); +/** + * int, buffer size used for sending and receiving + * data from sockets + */ +define("MC_BUFFER_SZ", 1024); +/** + * MemCached error numbers + */ +define("MC_ERR_NOT_ACTIVE", 1001); // no active servers +define("MC_ERR_SOCKET_WRITE", 1002); // socket_write() failed +define("MC_ERR_SOCKET_READ", 1003); // socket_read() failed +define("MC_ERR_SOCKET_CONNECT", 1004); // failed to connect to host +define("MC_ERR_DELETE", 1005); // delete() did not recieve DELETED command +define("MC_ERR_HOST_FORMAT", 1006); // sock_to_host() invalid host format +define("MC_ERR_HOST_DEAD", 1007); // sock_to_host() host is dead +define("MC_ERR_GET_SOCK", 1008); // get_sock() failed to find a valid socket +define("MC_ERR_SET", 1009); // _set() failed to receive the STORED response +define("MC_ERR_GET_KEY", 1010); // _load_items no values returned for key(s) +define("MC_ERR_LOADITEM_END", 1011); // _load_items failed to receive END response +define("MC_ERR_LOADITEM_BYTES", 1012); // _load_items bytes read larger than bytes available + + +/** + * MemCached PHP client Class. + * + * Communicates with the MemCached server, and executes the MemCached protocol + * MemCached available at http://www.danga.com/memcached + * + * @author Ryan Gilfether + * @package MemCachedClient + * @access public + * @version 1.0.7 + */ +class MemCachedClient +{ + /** + * array of servers no long available + * @var array + */ + var $host_dead; + /** + * array of open sockets + * @var array + */ + var $cache_sock; + /** + * determine if debugging is either on or off + * @var bool + */ + var $debug; + /** + * array of servers to attempt to use, "host:port" string format + * @var array + */ + var $servers; + /** + * count of currently active connections to servers + * @var int + */ + var $active; + /** + * error code if one is set + * @var int + */ + var $errno; + /** + * string describing error + * @var string + */ + var $errstr; + + + /** + * Constructor + * + * Creates a new MemCachedClient object + * Takes one parameter, a array of options. The most important key is + * $options["servers"], but that can also be set later with the set_servers() + * method. The servers must be an array of hosts, each of which is + * either a scalar of the form <10.0.0.10:11211> or an array of the + * former and an integer weight value. (the default weight if + * unspecified is 1.) It's recommended that weight values be kept as low + * as possible, as this module currently allocates memory for bucket + * distribution proportional to the total host weights. + * $options["debug"] turns the debugging on if set to true + * + * @access public + * @param array $option an array of servers and debug status + * @return object MemCachedClient the new MemCachedClient object + */ + function MemCachedClient($options = 0) + { + if(is_array($options)) + { + $this->set_servers($options["servers"]); + $this->debug = $options["debug"]; + $this->cache_sock = array(); + } + + $this->errno = 0; + $this->errstr = ""; + } + + + /** + * sets up the list of servers and the ports to connect to + * takes an array of servers in the same format as in the constructor + * + * @access public + * @param array $servers array of servers in the format described in the constructor + */ + function set_servers($servers) + { + $this->servers = $servers; + $this->active = count($this->servers); + } + + + /** + * if $do_debug is set to true, will print out + * debugging info, else debug is turned off + * + * @access public + * @param bool $do_debug set to true to turn debugging on, false to turn off + */ + function set_debug($do_debug) + { + $this->debug = $do_debug; + } + + + /** + * remove all cached hosts that are no longer good + * + * @access public + */ + function forget_dead_hosts() + { + unset($this->host_dead); + } + + + /** + * disconnects from all servers + * + * @access public + */ + function disconnect_all() + { + foreach($this->cache_sock as $sock) + socket_close($sock); + + unset($this->cache_sock); + $this->active = 0; + } + + + /** + * removes the key from the MemCache + * $time is the amount of time in seconds (or Unix time) until which + * the client wishes the server to refuse "add" and "replace" commands + * with this key. For this amount of item, the item is put into a + * delete queue, which means that it won't possible to retrieve it by + * the "get" command, but "add" and "replace" command with this key + * will also fail (the "set" command will succeed, however). After the + * time passes, the item is finally deleted from server memory. + * The parameter $time is optional, and, if absent, defaults to 0 + * (which means that the item will be deleted immediately and further + * storage commands with this key will succeed). + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_DELETE + * + * @access public + * @param string $key the key to delete + * @param timestamp $time optional, the amount of time server will refuse commands on key + * @return bool TRUE on success, FALSE if key does not exist + */ + function delete($key, $time = 0) + { + if(!$this->active) + { + $this->errno = MC_ERR_NOT_ACTIVE; + $this->errstr = "No active servers are available"; + + if($this->debug) + $this->_debug("delete(): There are no active servers available."); + + return FALSE; + } + + $sock = $this->get_sock($key); + + if(!is_resource($sock)) + { + $this->errno = MC_ERR_GET_SOCK; + $this->errstr = "Unable to retrieve a valid socket."; + + if($this->debug) + $this->_debug("delete(): get_sock() returned an invalid socket."); + + return FALSE; + } + + if(is_array($key)) + $key = $key[1]; + + $cmd = "delete $key $time\r\n"; + $cmd_len = strlen($cmd); + $offset = 0; + + // now send the command + while($offset < $cmd_len) + { + $result = socket_write($sock, substr($cmd, $offset, MC_BUFFER_SZ), MC_BUFFER_SZ); + + if($result !== FALSE) + $offset += $result; + else if($offset < $cmd_len) + { + $this->errno = MC_ERR_SOCKET_WRITE; + $this->errstr = "Failed to write to socket."; + + if($this->debug) + { + $sockerr = socket_last_error($sock); + $this->_debug("delete(): socket_write() returned FALSE. Socket Error $sockerr: ".socket_strerror($sockerr)); + } + + return FALSE; + } + } + + // now read the server's response + if(($retval = socket_read($sock, MC_BUFFER_SZ, PHP_NORMAL_READ)) === FALSE) + { + $this->errno = MC_ERR_SOCKET_READ; + $this->errstr = "Failed to read from socket."; + + if($this->debug) + { + $sockerr = socket_last_error($sock); + $this->_debug("delete(): socket_read() returned FALSE. Socket Error $sockerr: ".socket_strerror($sockerr)); + } + + return FALSE; + } + + // remove the \r\n from the end + $retval = rtrim($retval); + + // now read the server's response + if($retval == "DELETED") + return TRUE; + else + { + // something went wrong, create the error + $this->errno = MC_ERR_DELETE; + $this->errstr = "Failed to receive DELETED response from server."; + + if($this->debug) + $this->_debug("delete(): Failed to receive DELETED response from server. Received $retval instead."); + + return FALSE; + } + } + + + /** + * Like set(), but only stores in memcache if the key doesn't already exist. + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_SET + * + * @access public + * @param string $key the key to set + * @param mixed $val the value of the key + * @param timestamp $exptime optional, the to to live of the key + * @return bool TRUE on success, else FALSE + */ + function add($key, $val, $exptime = 0) + { + return $this->_set("add", $key, $val, $exptime); + } + + + /** + * Like set(), but only stores in memcache if the key already exists. + * returns TRUE on success else FALSE + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_SET + * + * @access public + * @param string $key the key to set + * @param mixed $val the value of the key + * @param timestamp $exptime optional, the to to live of the key + * @return bool TRUE on success, else FALSE + */ + function replace($key, $val, $exptime = 0) + { + return $this->_set("replace", $key, $val, $exptime); + } + + + /** + * Unconditionally sets a key to a given value in the memcache. Returns true + * if it was stored successfully. + * The $key can optionally be an arrayref, with the first element being the + * hash value, as described above. + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_SET + * + * @access public + * @param string $key the key to set + * @param mixed $val the value of the key + * @param timestamp $exptime optional, the to to live of the key + * @return bool TRUE on success, else FALSE + */ + function set($key, $val, $exptime = 0) + { + return $this->_set("set", $key, $val, $exptime); + } + + + /** + * Retrieves a key from the memcache. Returns the value (automatically + * unserialized, if necessary) or FALSE if it fails. + * The $key can optionally be an array, with the first element being the + * hash value, if you want to avoid making this module calculate a hash + * value. You may prefer, for example, to keep all of a given user's + * objects on the same memcache server, so you could use the user's + * unique id as the hash value. + * Possible errors set are: + * MC_ERR_GET_KEY + * + * @access public + * @param string $key the key to retrieve + * @return mixed the value of the key, FALSE on error + */ + function get($key) + { + $val =& $this->get_multi($key); + + if(!$val) + { + $this->errno = MC_ERR_GET_KEY; + $this->errstr = "No value found for key $key"; + + if($this->debug) + $this->_debug("get(): No value found for key $key"); + + return FALSE; + } + + return $val[$key]; + } + + + /** + * just like get(), but takes an array of keys, returns FALSE on error + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * + * @access public + * @param array $keys the keys to retrieve + * @return array the value of each key, FALSE on error + */ + function get_multi($keys) + { + $sock_keys = array(); + $socks = array(); + $val = 0; + + if(!$this->active) + { + $this->errno = MC_ERR_NOT_ACTIVE; + $this->errstr = "No active servers are available"; + + if($this->debug) + $this->_debug("get_multi(): There are no active servers available."); + + return FALSE; + } + + if(!is_array($keys)) + { + $arr[] = $keys; + $keys = $arr; + } + + foreach($keys as $k) + { + $sock = $this->get_sock($k); + + if($sock) + { + $k = is_array($k) ? $k[1] : $k; + + if(@!is_array($sock_keys[$sock])) + $sock_keys[$sock] = array(); + + // if $sock_keys[$sock] doesn't exist, create it + if(!$sock_keys[$sock]) + $socks[] = $sock; + + $sock_keys[$sock][] = $k; + } + } + + if(!is_array($socks)) + { + $arr[] = $socks; + $socks = $arr; + } + + foreach($socks as $s) + { + $this->_load_items($s, $val, $sock_keys[$sock]); + } + + if($this->debug) + { + while(list($k, $v) = @each($val)) + $this->_debug("MemCache: got $k = $v\n"); + } + + return $val; + } + + + /** + * Sends a command to the server to atomically increment the value for + * $key by $value, or by 1 if $value is undefined. Returns FALSE if $key + * doesn't exist on server, otherwise it returns the new value after + * incrementing. Value should be zero or greater. Overflow on server + * is not checked. Be aware of values approaching 2**32. See decr. + * ONLY WORKS WITH NUMERIC VALUES + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * + * @access public + * @param string $key the keys to increment + * @param int $value the amount to increment the key bye + * @return int the new value of the key, else FALSE + */ + function incr($key, $value = 1) + { + return $this->_incrdecr("incr", $key, $value); + } + + + /** + * Like incr, but decrements. Unlike incr, underflow is checked and new + * values are capped at 0. If server value is 1, a decrement of 2 + * returns 0, not -1. + * ONLY WORKS WITH NUMERIC VALUES + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * + * @access public + * @param string $key the keys to increment + * @param int $value the amount to increment the key bye + * @return int the new value of the key, else FALSE + */ + function decr($key, $value = 1) + { + return $this->_incrdecr("decr", $key, $value); + } + + + /** + * When a function returns FALSE, an error code is set. + * This funtion will return the error code. + * See error_string() + * + * @access public + * @return int the value of the last error code + */ + function error() + { + return $this->errno; + } + + + /** + * Returns a string describing the error set in error() + * See error() + * + * @access public + * @return int a string describing the error code given + */ + function error_string() + { + return $this->errstr; + } + + + /** + * Resets the error number and error string + * + * @access public + */ + function error_clear() + { + // reset to no error + $this->errno = 0; + $this->errstr = ""; + } + + + /* + * PRIVATE FUNCTIONS + */ + + + /** + * connects to a server + * The $host may either a string int the form of host:port or an array of the + * former and an integer weight value. (the default weight if + * unspecified is 1.) See the constructor for details + * Possible errors set are: + * MC_ERR_HOST_FORMAT + * MC_ERR_HOST_DEAD + * MC_ERR_SOCKET_CONNECT + * + * @access private + * @param mixed $host either an array or a string + * @return resource the socket of the new connection, else FALSE + */ + function sock_to_host($host) + { + if(is_array($host)) + $host = array_shift($host); + + $now = time(); + + // seperate the ip from the port, index 0 = ip, index 1 = port + $conn = explode(":", $host); + if(count($conn) != 2) + { + $this->errno = MC_ERR_HOST_FORMAT; + $this->errstr = "Host address was not in the format of host:port"; + + if($this->debug) + $this->_debug("sock_to_host(): Host address was not in the format of host:port"); + + return FALSE; + } + + if(@($this->host_dead[$host] && $this->host_dead[$host] > $now) || + @($this->host_dead[$conn[0]] && $this->host_dead[$conn[0]] > $now)) + { + $this->errno = MC_ERR_HOST_DEAD; + $this->errstr = "Host $host is not available."; + + if($this->debug) + $this->_debug("sock_to_host(): Host $host is not available."); + + return FALSE; + } + + // connect to the server, if it fails, add it to the host_dead below + $sock = socket_create (AF_INET, SOCK_STREAM, getprotobyname("TCP")); + + // we need surpress the error message if a connection fails + if(!@socket_connect($sock, $conn[0], $conn[1])) + { + $this->host_dead[$host]=$this->host_dead[$conn[0]]=$now+60+intval(rand(0, 10)); + + $this->errno = MC_ERR_SOCKET_CONNECT; + $this->errstr = "Failed to connect to ".$conn[0].":".$conn[1]; + + if($this->debug) + $this->_debug("sock_to_host(): Failed to connect to ".$conn[0].":".$conn[1]); + + return FALSE; + } + + // success, add to the list of sockets + $cache_sock[$host] = $sock; + + return $sock; + } + + + /** + * retrieves the socket associated with a key + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * + * @access private + * @param string $key the key to retrieve the socket from + * @return resource the socket of the connection, else FALSE + */ + function get_sock($key) + { + $buckets = 0; + + if(!$this->active) + { + $this->errno = MC_ERR_NOT_ACTIVE; + $this->errstr = "No active servers are available"; + + if($this->debug) + $this->_debug("get_sock(): There are no active servers available."); + + return FALSE; + } + + $hv = is_array($key) ? intval($key[0]) : $this->_hashfunc($key); + + if(!$buckets) + { + $bu = $buckets = array(); + + foreach($this->servers as $v) + { + if(is_array($v)) + { + for($i = 1; $i <= $v[1]; ++$i) + $bu[] = $v[0]; + } + else + $bu[] = $v; + } + + $buckets = $bu; + } + + $real_key = is_array($key) ? $key[1] : $key; + $tries = 0; + while($tries < 20) + { + $host = @$buckets[$hv % count($buckets)]; + $sock = $this->sock_to_host($host); + + if(is_resource($sock)) + return $sock; + + $hv += $this->_hashfunc($tries.$real_key); + ++$tries; + } + + $this->errno = MC_ERR_GET_SOCK; + $this->errstr = "Unable to retrieve a valid socket."; + + if($this->debug) + $this->_debug("get_sock(): Unable to retrieve a valid socket."); + + return FALSE; + } + + + /** + * increments or decrements a numerical value in memcached. this function is + * called from incr() and decr() + * ONLY WORKS WITH NUMERIC VALUES + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * + * @access private + * @param string $cmdname the command to send, either incr or decr + * @param string $key the key to perform the command on + * @param mixed $value the value to incr or decr the key value by + * @return int the new value of the key, FALSE if something went wrong + */ + function _incrdecr($cmdname, $key, $value) + { + if(!$this->active) + { + $this->errno = MC_ERR_NOT_ACTIVE; + $this->errstr = "No active servers are available"; + + if($this->debug) + $this->_debug("_incrdecr(): There are no active servers available."); + + return FALSE; + } + + $sock = $this->get_sock($key); + if(!is_resource($sock)) + { + $this->errno = MC_ERR_GET_SOCK; + $this->errstr = "Unable to retrieve a valid socket."; + + if($this->debug) + $this->_debug("_incrdecr(): Invalid socket returned by get_sock()."); + + return FALSE; + } + + if($value == "") + $value = 1; + + $cmd = "$cmdname $key $value\r\n"; + $cmd_len = strlen($cmd); + $offset = 0; + + // write the command to the server + while($offset < $cmd_len) + { + $result = socket_write($sock, substr($cmd, $offset, MC_BUFFER_SZ), MC_BUFFER_SZ); + + if($result !== FALSE) + $offset += $result; + else if($offset < $cmd_len) + { + $this->errno = MC_ERR_SOCKET_WRITE; + $this->errstr = "Failed to write to socket."; + + if($this->debug) + { + $sockerr = socket_last_error($sock); + $this->_debug("_incrdecr(): socket_write() returned FALSE. Error $errno: ".socket_strerror($sockerr)); + } + + return FALSE; + } + } + + // now read the server's response + if(($retval = socket_read($sock, MC_BUFFER_SZ, PHP_NORMAL_READ)) === FALSE) + { + $this->errno = MC_ERR_SOCKET_READ; + $this->errstr = "Failed to read from socket."; + + if($this->debug) + { + $sockerr = socket_last_error($sock); + $this->_debug("_incrdecr(): socket_read() returned FALSE. Socket Error $errno: ".socket_strerror($sockerr)); + } + + return FALSE; + } + + // strip the /r/n from the end and return value + return trim($retval); + } + + + /** + * sends the command to the server + * Possible errors set are: + * MC_ERR_NOT_ACTIVE + * MC_ERR_GET_SOCK + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_SET + * + * @access private + * @param string $cmdname the command to send, either incr or decr + * @param string $key the key to perform the command on + * @param mixed $value the value to set the key to + * @param timestamp $exptime expiration time of the key + * @return bool TRUE on success, else FALSE + */ + function _set($cmdname, $key, $val, $exptime = 0) + { + if(!$this->active) + { + $this->errno = MC_ERR_NOT_ACTIVE; + $this->errstr = "No active servers are available"; + + if($this->debug) + $this->_debug("_set(): No active servers are available."); + + return FALSE; + } + + $sock = $this->get_sock($key); + if(!is_resource($sock)) + { + $this->errno = MC_ERR_GET_SOCK; + $this->errstr = "Unable to retrieve a valid socket."; + + if($this->debug) + $this->_debug("_set(): Invalid socket returned by get_sock()."); + + return FALSE; + } + + $flags = 0; + $key = is_array($key) ? $key[1] : $key; + + $raw_val = $val; + + // if the value is not scalar, we need to serialize it + if(!is_scalar($val)) + { + $val = serialize($val); + $flags |= 1; + } + + $len = strlen($val); + if (!is_int($exptime)) + $exptime = 0; + + // send off the request + $cmd = "$cmdname $key $flags $exptime $len\r\n$val\r\n"; + $cmd_len = strlen($cmd); + $offset = 0; + + // write the command to the server + while($offset < $cmd_len) + { + $result = socket_write($sock, substr($cmd, $offset, MC_BUFFER_SZ), MC_BUFFER_SZ); + + if($result !== FALSE) + $offset += $result; + else if($offset < $cmd_len) + { + $this->errno = MC_ERR_SOCKET_WRITE; + $this->errstr = "Failed to write to socket."; + + if($this->debug) + { + $errno = socket_last_error($sock); + $this->_debug("_set(): socket_write() returned FALSE. Error $errno: ".socket_strerror($errno)); + } + + return FALSE; + } + } + + // now read the server's response + if(($l_szResponse = socket_read($sock, 6, PHP_NORMAL_READ)) === FALSE) + { + $this->errno = MC_ERR_SOCKET_READ; + $this->errstr = "Failed to read from socket."; + + if($this->debug) + { + $errno = socket_last_error($sock); + $this->_debug("_set(): socket_read() returned FALSE. Error $errno: ".socket_strerror($errno)); + } + + return FALSE; + } + + if($l_szResponse == "STORED") + { + if($this->debug) + $this->_debug("MemCache: $cmdname $key = $raw_val"); + + return TRUE; + } + + $this->errno = MC_ERR_SET; + $this->errstr = "Failed to receive the STORED response from the server."; + + if($this->debug) + $this->_debug("_set(): Did not receive STORED as the server response! Received $l_szResponse instead."); + + return FALSE; + } + + + /** + * retrieves the value, and returns it unserialized + * Possible errors set are: + * MC_ERR_SOCKET_WRITE + * MC_ERR_SOCKET_READ + * MC_ERR_GET_KEY + * MC_ERR_LOADITEM_END + * MC_ERR_LOADITEM_BYTES + * + * @access private + * @param resource $sock the socket to connection we are retriving from + * @param array $val reference to the values retrieved + * @param mixed $sock_keys either a string or an array of keys to retrieve + * @return array TRUE on success, else FALSE + */ + function _load_items($sock, &$val, $sock_keys) + { + $val = array(); + $cmd = "get "; + + if(!is_array($sock_keys)) + { + $arr[] = $sock_keys; + $sock_keys = $arr; + } + + foreach($sock_keys as $sk) + $cmd .= $sk." "; + + $cmd .="\r\n"; + $cmd_len = strlen($cmd); + $offset = 0; + + // write the command to the server + while($offset < $cmd_len) + { + $result = socket_write($sock, substr($cmd, $offset, MC_BUFFER_SZ), MC_BUFFER_SZ); + + if($result !== FALSE) + $offset += $result; + else if($offset < $cmd_len) + { + $this->errno = MC_ERR_SOCKET_WRITE; + $this->errstr = "Failed to write to socket."; + + if($this->debug) + { + $errno = socket_last_error($sock); + $this->_debug("_load_items(): socket_write() returned FALSE. Error $errno: ".socket_strerror($errno)); + } + + return FALSE; + } + } + + $len = 0; + $buf = ""; + $flags_array = array(); + + // now read the response from the server + while($line = socket_read($sock, MC_BUFFER_SZ, PHP_BINARY_READ)) + { + // check for a socket_read error + if($line === FALSE) + { + $this->errno = MC_ERR_SOCKET_READ; + $this->errstr = "Failed to read from socket."; + + if($this->debug) + { + $errno = socket_last_error($sock); + $this->_debug("_load_items(): socket_read() returned FALSE. Error $errno: ".socket_strerror($errno)); + } + + return FALSE; + } + + if($len == 0) + { + $header = substr($line, 0, strpos($line, "\r\n")); + $matches = explode(" ", $header); + + if(is_string($matches[1]) && is_numeric($matches[2]) && is_numeric($matches[3])) + { + $rk = $matches[1]; + $flags = $matches[2]; + $len = $matches[3]; + + if($flags) + $flags_array[$rk] = $flags; + + $len_array[$rk] = $len; + $bytes_read = 0; + + // get the left over data after the header is read + $line = substr($line, strpos($line, "\r\n")+1, strlen($line)); + } + else + { + $this->errno = MC_ERR_GET_KEY; + $this->errstr = "Requested key(s) returned no values."; + + // something went wrong, we never recieved the header + if($this->debug) + $this->_debug("_load_items(): Requested key(s) returned no values."); + + return FALSE; + } + } + + // skip over the extra return or newline + if($line == "\r" || $line == "\n") + continue; + + $bytes_read += strlen($line); + $buf .= $line; + + // we read the all of the data, take in account + // for the /r/nEND/r/n + if($bytes_read == ($len + 7)) + { + $end = substr($buf, $len+2, 3); + if($end == "END") + { + $val[$rk] = substr($buf, 0, $len); + + foreach($sock_keys as $sk) + { + if(!isset($val[$sk])) + continue; + + if(strlen($val[$sk]) != $len_array[$sk]) + continue; + + if(@$flags_array[$sk] & 1) + $val[$sk] = unserialize($val[$sk]); + } + + return TRUE; + } + else + { + $this->errno = MC_ERR_LOADITEM_END; + $this->errstr = "Failed to receive END response from server."; + + if($this->debug) + $this->_debug("_load_items(): Failed to receive END. Received $end instead."); + + return FALSE; + } + } + + // take in consideration for the "\r\nEND\r\n" + if($bytes_read > ($len + 7)) + { + $this->errno = MC_ERR_LOADITEM_BYTES; + $this->errstr = "Bytes read from server greater than size of data."; + + if($this->debug) + $this->_debug("_load_items(): Bytes read is greater than requested data size."); + + return FALSE; + } + + } + } + + + /** + * creates our hash + * + * @access private + * @param int $num + * @return hash + */ + function _hashfunc($num) + { + $hash = 0; + + foreach(preg_split('//', $num, -1, PREG_SPLIT_NO_EMPTY) as $v) + { + $hash = $hash * 33 + ord($v); + } + + return $hash; + } + + /** + * function that can be overridden to handle debug output + * by default debug info is print to the screen + * + * @access private + * @param $text string to output debug info + */ + function _debug($text) + { + print $text . "\r\n"; + } +} + +?> \ No newline at end of file diff --git a/includes/Setup.php b/includes/Setup.php index 7b0c64670c..fd2a0f85c7 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -17,9 +17,25 @@ include_once( "$IP/User.php" ); include_once( "$IP/LinkCache.php" ); include_once( "$IP/Title.php" ); include_once( "$IP/Article.php" ); +require( "$IP/MemCachedClient.inc.php" ); + +wfDebug( "\n\n" ); global $wgUser, $wgLang, $wgOut, $wgTitle; global $wgArticle, $wgDeferredUpdateList, $wgLinkCache; +global $wgMemc; + +class MemCachedClientforWiki extends MemCachedClient { + function _debug( $text ) { + wfDebug( "memcached: $text\n" ); + } +} + +$wgMemc = new MemCachedClientforWiki(); +if( $wgUseMemCached ) { + $wgMemc->set_servers( $wgMemCachedServers ); + $wgMemc->set_debug( $wgMemcachedDebug ); +} $wgOut = new OutputPage(); $wgLangClass = "Language" . ucfirst( $wgLanguageCode ); @@ -29,8 +45,7 @@ if( ! class_exists( $wgLangClass ) ) { } $wgLang = new $wgLangClass(); -$wgUser = new User(); -$wgUser->loadFromSession(); +$wgUser = User::loadFromSession(); $wgDeferredUpdateList = array(); $wgLinkCache = new LinkCache(); diff --git a/includes/User.php b/includes/User.php index 8306392a71..ffef535572 100644 --- a/includes/User.php +++ b/includes/User.php @@ -129,23 +129,22 @@ class User { return $this->mBlockreason; } - function loadFromSession() + /* static */ function loadFromSession() { global $HTTP_COOKIE_VARS, $wsUserID, $wsUserName, $wsUserPassword; + global $wgMemc, $wgDBname; if ( isset( $wsUserID ) ) { if ( 0 != $wsUserID ) { $sId = $wsUserID; } else { - $this->mId = 0; - return; + return new User(); } } else if ( isset( $HTTP_COOKIE_VARS["wcUserID"] ) ) { $sId = $HTTP_COOKIE_VARS["wcUserID"]; $wsUserID = $sId; } else { - $this->mId = 0; - return; + return new User(); } if ( isset( $wsUserName ) ) { $sName = $wsUserName; @@ -153,30 +152,40 @@ class User { $sName = $HTTP_COOKIE_VARS["wcUserName"]; $wsUserName = $sName; } else { - $this->mId = 0; - return; + return new User(); } $passwordCorrect = FALSE; - $this->mId = $sId; - $this->loadFromDatabase(); + $user = $wgMemc->get( $key = "$wgDBname:user:user_id:$sId" ); + if($makenew = !$user) { + wfDebug( "User::loadFromSession() unable to load from memcached\n" ); + $user = new User(); + $user->mId = $sId; + $user->loadFromDatabase(); + } else { + wfDebug( "User::loadFromSession() got from cache!\n" ); + } if ( isset( $wsUserPassword ) ) { - $passwordCorrect = $wsUserPassword == $this->mPassword; + $passwordCorrect = $wsUserPassword == $user->mPassword; } else if ( isset( $HTTP_COOKIE_VARS["wcUserPassword"] ) ) { - $this->mCookiePassword = $HTTP_COOKIE_VARS["wcUserPassword"]; - $wsUserPassword = $this->addSalt( $this->mCookiePassword ); - $passwordCorrect = $wsUserPassword == $this->mPassword; + $user->mCookiePassword = $HTTP_COOKIE_VARS["wcUserPassword"]; + $wsUserPassword = $user->addSalt( $user->mCookiePassword ); + $passwordCorrect = $wsUserPassword == $user->mPassword; } else { - $this->mId = 0; - $this->loadDefaults(); # Can't log in from session - return; + return new User(); # Can't log in from session } - if ( ( $sName == $this->mName ) && $passwordCorrect ) { - return; + if ( ( $sName == $user->mName ) && $passwordCorrect ) { + if($makenew) { + if($wgMemc->set( $key, $user )) + wfDebug( "User::loadFromSession() successfully saved user\n" ); + else + wfDebug( "User::loadFromSession() unable to save to memcached\n" ); + } + return $user; } - $this->loadDefaults(); # Can't log in from session + return new User(); # Can't log in from session } function loadFromDatabase() @@ -491,7 +500,7 @@ class User { function saveSettings() { - global $wgUser; + global $wgMemc, $wgDBname; if ( ! $this->mNewtalk ) { if( $this->mId ) { @@ -515,6 +524,8 @@ class User { "user_touched= '" . wfStrencode( $this->mTouched ) . "' WHERE user_id={$this->mId}"; wfQuery( $sql, "User::saveSettings" ); + #$wgMemc->replace( "$wgDBname:user:user_id:$this->mId", $this ); + $wgMemc->delete( "$wgDBname:user:user_id:$this->mId" ); } # Checks if a user with the given name exists diff --git a/includes/UserTalkUpdate.php b/includes/UserTalkUpdate.php index 011a7ba711..777f5f4c81 100644 --- a/includes/UserTalkUpdate.php +++ b/includes/UserTalkUpdate.php @@ -15,7 +15,7 @@ class UserTalkUpdate { function doUpdate() { - global $wgUser, $wgLang; + global $wgUser, $wgLang, $wgMemc, $wgDBname; $fname = "UserTalkUpdate::doUpdate"; # If namespace isn't User_talk:, do nothing. @@ -27,7 +27,6 @@ class UserTalkUpdate { # If the user talk page is our own, clear the flag # whether we are reading it or writing it. if ( 0 == strcmp( $this->mTitle, $wgUser->getName() ) ) { - $wgUser->setNewtalk( 0 ); $wgUser->saveSettings(); @@ -39,13 +38,12 @@ class UserTalkUpdate { $user->setID(User::idFromName($this->mTitle)); if ($id=$user->getID()) { $sql = "INSERT INTO user_newtalk (user_id) values ({$id})"; - + $wgMemc->delete( "$wgDBname:user:user_id:$id" ); } else { #anon if(preg_match("/^\d{1,3}\.\d{1,3}.\d{1,3}\.\d{1,3}$/",$this->mTitle)) { #real anon (user:xxx.xxx.xxx.xxx) $sql = "INSERT INTO user_newtalk (user_id,user_ip) values (0,\"{$this->mTitle}\")"; - } } -- 2.20.1