* Split off DB load monitoring logic into a LoadMonitor class hierarchy, to allow...
authorTim Starling <tstarling@users.mediawiki.org>
Mon, 7 Jul 2008 03:31:00 +0000 (03:31 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Mon, 7 Jul 2008 03:31:00 +0000 (03:31 +0000)
* Use an associative array to initialise LoadBalancer objects
* By default, use Preprocessor_DOM if available, otherwise use Preprocessor_Hash. Preprocessor_Hash has worse performance.
* Fix parserTests.php for replicated databases. Use CREATE TABLE instead of CREATE TEMPORARY TABLE if there is more than one server configured.
* Log exceptions even in command-line mode.

includes/AutoLoader.php
includes/DefaultSettings.php
includes/Exception.php
includes/db/LBFactory.php
includes/db/LBFactory_Multi.php
includes/db/LoadBalancer.php
includes/db/LoadMonitor.php [new file with mode: 0644]
includes/parser/Parser.php
maintenance/importDump.php
maintenance/parserTests.inc

index f1d8a7d..e90491b 100644 (file)
@@ -302,6 +302,8 @@ class AutoLoader {
                'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
                'LBFactory_Simple' => 'includes/db/LBFactory.php',
                'LoadBalancer' => 'includes/db/LoadBalancer.php',
+               'LoadMonitor' => 'includes/db/LoadMonitor.php',
+               'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
                'MSSQLField' => 'includes/db/DatabaseMssql.php',
                'MySQLField' => 'includes/db/Database.php',
                'MySQLMasterPos' => 'includes/db/Database.php',
index 07423a4..537205b 100644 (file)
@@ -3190,11 +3190,17 @@ $wgSlaveLagCritical = 30;
  * Parser configuration. Associative array with the following members:
  *
  *  class             The class name
- *  preprocessorClass The preprocessor class, by default it is Preprocessor_Hash.
- *                    Preprocessor_DOM is also available and better tested, but
- *                    it has a dependency of the dom module of PHP.
- *                    It has no effect with Parser_OldPP parser class.
  *
+ *  preprocessorClass The preprocessor class. Two classes are currently available:
+ *                    Preprocessor_Hash, which uses plain PHP arrays for tempoarary 
+ *                    storage, and Preprocessor_DOM, which uses the DOM module for 
+ *                    temporary storage. Preprocessor_DOM generally uses less memory;
+ *                    the speed of the two is roughly the same.
+ *                    
+ *                    If this parameter is not given, it uses Preprocessor_DOM if the 
+ *                    DOM module is available, otherwise it uses Preprocessor_Hash.
+ *
+ *                    Has no effect on Parser_OldPP.
  *
  * The entire associative array will be passed through to the constructor as
  * the first parameter. Note that only Setup.php can use this variable --
@@ -3205,7 +3211,7 @@ $wgSlaveLagCritical = 30;
  */
 $wgParserConf = array(
        'class' => 'Parser',
-       'preprocessorClass' => 'Preprocessor_Hash',
+       #'preprocessorClass' => 'Preprocessor_Hash',
 );
 
 /**
index 2ba2946..7482020 100644 (file)
@@ -164,13 +164,13 @@ class MWException extends Exception {
         */
        function report() {
                global $wgCommandLineMode;
+               $log = $this->getLogMessage();
+               if ( $log ) {
+                       wfDebugLog( 'exception', $log );
+               }
                if ( $wgCommandLineMode ) {
                        fwrite( STDERR, $this->getText() );
                } else {
-                       $log = $this->getLogMessage();
-                       if ( $log ) {
-                               wfDebugLog( 'exception', $log );
-                       }
                        $this->reportHTML();
                }
        }
index e7b2778..b354788 100644 (file)
@@ -118,7 +118,10 @@ class LBFactory_Simple extends LBFactory {
                                ));
                        }
 
-                       $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
+                       $this->mainLB = new LoadBalancer( array(
+                               'servers' => $wgDBservers, 
+                               'masterWaitTimeout' => $wgMasterWaitTimeout 
+                       ));
                        $this->mainLB->parentInfo( array( 'id' => 'main' ) );
                        $this->chronProt->initLB( $this->mainLB );
                }
@@ -131,7 +134,9 @@ class LBFactory_Simple extends LBFactory {
                        if ( !isset( $wgExternalServers[$cluster] ) ) {
                                throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
                        }
-                       $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
+                       $this->extLBs[$cluster] = new LoadBalancer( array(
+                               'servers' => $wgExternalServers[$cluster] 
+                       ));
                        $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
                }
                return $this->extLBs[$cluster];
index b5fc1f6..68eee56 100644 (file)
@@ -131,7 +131,10 @@ class LBFactory_Multi extends LBFactory {
        function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
                global $wgMasterWaitTimeout;
                $servers = $this->makeServerArray( $template, $loads, $groupLoads );
-               $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
+               $lb = new LoadBalancer( array(
+                       'servers' => $servers,
+                       'masterWaitTimeout' => $wgMasterWaitTimeout 
+               ));
                $lb->parentInfo( array( 'id' => $id ) );
                return $lb;
        }
index 4280713..42c4044 100644 (file)
@@ -17,11 +17,33 @@ class LoadBalancer {
        /* private */ var $mWaitForPos, $mWaitTimeout;
        /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
        /* private */ var $mParentInfo, $mLagTimes;
+       /* private */ var $mLoadMonitorClass, $mLoadMonitor;
 
-       function __construct( $servers, $failFunction = false, $waitTimeout = 10, $unused = false )
+       /**
+        * @param array $params Array with keys:
+        *    servers           Required. Array of server info structures.
+        *    failFunction          Deprecated, use exceptions instead.
+        *    masterWaitTimeout Replication lag wait timeout
+        *    loadMonitor       Name of a class used to fetch server lag and load.
+        */
+       function __construct( $params )
        {
-               $this->mServers = $servers;
-               $this->mFailFunction = $failFunction;
+               if ( !isset( $params['servers'] ) ) {
+                       throw new MWException( __CLASS__.': missing servers parameter' );
+               }
+               $this->mServers = $params['servers'];
+
+               if ( isset( $params['failFunction'] ) ) {
+                       $this->mFailFunction = $params['failFunction'];
+               } else {
+                       $this->mFailFunction = false;
+               }
+               if ( isset( $params['waitTimeout'] ) ) {
+                       $this->mWaitTimeout = $params['waitTimeout'];
+               } else {
+                       $this->mWaitTimeout = 10;
+               }
+
                $this->mReadIndex = -1;
                $this->mWriteIndex = -1;
                $this->mConns = array(
@@ -31,12 +53,13 @@ class LoadBalancer {
                $this->mLastIndex = -1;
                $this->mLoads = array();
                $this->mWaitForPos = false;
-               $this->mWaitTimeout = $waitTimeout;
                $this->mLaggedSlaveMode = false;
                $this->mErrorConnection = false;
                $this->mAllowLag = false;
+               $this->mLoadMonitorClass = isset( $params['loadMonitor'] ) 
+                       ? $params['loadMonitor'] : 'LoadMonitor_MySQL';
 
-               foreach( $servers as $i => $server ) {
+               foreach( $params['servers'] as $i => $server ) {
                        $this->mLoads[$i] = $server['load'];
                        if ( isset( $server['groupLoads'] ) ) {
                                foreach ( $server['groupLoads'] as $group => $ratio ) {
@@ -54,6 +77,17 @@ class LoadBalancer {
                return new LoadBalancer( $servers, $failFunction, $waitTimeout );
        }
 
+       /**
+        * Get a LoadMonitor instance
+        */
+       function getLoadMonitor() {
+               if ( !isset( $this->mLoadMonitor ) ) {
+                       $class = $this->mLoadMonitorClass;
+                       $this->mLoadMonitor = new $class( $this );
+               }
+               return $this->mLoadMonitor;
+       }
+
        /**
         * Get or set arbitrary data used by the parent object, usually an LBFactory
         */
@@ -178,6 +212,9 @@ class LoadBalancer {
                        throw new MWException( "Empty server array given to LoadBalancer" );
                }
 
+               # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
+               $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
+
                $i = false;
                $found = false;
                $laggedSlaveMode = false;
@@ -203,7 +240,8 @@ class LoadBalancer {
 
                                if ( $i === false ) {
                                        # pickRandom() returned false
-                                       # This is permanent and means the configuration wants us to return false
+                                       # This is permanent and means the configuration or the load monitor 
+                                       # wants us to return false.
                                        wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
                                        wfProfileOut( __METHOD__ );
                                        return false;
@@ -219,25 +257,24 @@ class LoadBalancer {
                                        continue;
                                }
 
-                               if ( isset( $this->mServers[$i]['max threads'] ) ) {
-                                       $status = $conn->getStatus("Thread%");
-                                       if ( $wiki !== false ) {
-                                               $this->reuseConnection( $conn );
-                                       }
-                                       if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
-                                               $totalThreadsConnected += $status['Threads_connected'];
-                                               $overloadedServers++;
-                                               unset( $currentLoads[$i] );
-                                       } else {
-                                               # Max threads satisfied, return this server
-                                               break 2;
-                                       }
+                               // Perform post-connection backoff
+                               $threshold = isset( $this->mServers[$i]['max threads'] ) 
+                                       ? $this->mServers[$i]['max threads'] : false;
+                               $backoff = $this->getLoadMonitor()->postConnectionBackoff( $conn, $threshold );
+
+                               // Decrement reference counter, we are finished with this connection.
+                               // It will be incremented for the caller later.
+                               if ( $wiki !== false ) {
+                                       $this->reuseConnection( $conn );
+                               }
+                               
+                               if ( $backoff ) {
+                                       # Post-connection overload, don't use this server for now
+                                       $totalThreadsConnected += $backoff;
+                                       $overloadedServers++;
+                                       unset( $currentLoads[$i] );
                                } else {
-                                       # No maximum, return this server
-                                       if ( $wiki !== false ) {
-                                               $this->reuseConnection( $conn );
-                                       }
-                                       $found = true;
+                                       # Return this server
                                        break 2;
                                }
                        }
@@ -269,6 +306,7 @@ class LoadBalancer {
                }
 
                if ( $i !== false ) {
+                       # Slave connection successful
                        # Wait for the session master pos for a short time
                        if ( $this->mWaitForPos && $i > 0 ) {
                                if ( !$this->doWait( $i ) ) {
@@ -679,6 +717,7 @@ class LoadBalancer {
 
        /**
         * Get the host name or IP address of the server with the specified index
+        * Prefer a readable name if available.
         */
        function getServerName( $i ) {
                if ( isset( $this->mServers[$i]['hostName'] ) ) {
@@ -690,6 +729,17 @@ class LoadBalancer {
                }
        }
 
+       /**
+        * Return the server info structure for a given index, or false if the index is invalid.
+        */
+       function getServerInfo( $i ) {
+               if ( isset( $this->mServers[$i] ) ) {
+                       return $this->mServers[$i];
+               } else {
+                       return false;
+               }
+       }
+
        /**
         * Get the current master position for chronology control purposes
         * @return mixed
@@ -813,6 +863,20 @@ class LoadBalancer {
                return $success;
        }
 
+       /**
+        * Call a function with each open connection object
+        */
+       function forEachOpenConnection( $callback, $params = array() ) {
+               foreach ( $this->mConns as $conns2 ) {
+                       foreach ( $conns2 as $conns3 ) {
+                               foreach ( $conns3 as $conn ) {
+                                       $mergedParams = array_merge( array( $conn ), $params );
+                                       call_user_func_array( $callback, $mergedParams );
+                               }
+                       }
+               }
+       }
+
        /**
         * Get the hostname and lag time of the most-lagged slave.
         * This is useful for maintenance scripts that need to throttle their updates.
@@ -843,52 +907,12 @@ class LoadBalancer {
         * Results are cached for a short time in memcached, and indefinitely in the process cache
         */
        function getLagTimes( $wiki = false ) {
-               wfProfileIn( __METHOD__ );
-
-               if ( !isset( $this->mLagTimes ) ) {
-                       $expiry = 5;
-                       $requestRate = 10;
-
-                       global $wgMemc;
-                       $masterName = $this->getServerName( 0 );
-                       $memcKey = wfMemcKey( 'lag_times', $masterName );
-                       $times = $wgMemc->get( $memcKey );
-                       if ( $times ) {
-                               # Randomly recache with probability rising over $expiry
-                               $elapsed = time() - $times['timestamp'];
-                               $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
-                               if ( mt_rand( 0, $chance ) != 0 ) {
-                                       unset( $times['timestamp'] );
-                                       wfProfileOut( __METHOD__ );
-                                       return $times;
-                               }
-                               wfIncrStats( 'lag_cache_miss_expired' );
-                       } else {
-                               wfIncrStats( 'lag_cache_miss_absent' );
-                       }
-
-                       # Cache key missing or expired
-
-                       $times = array();
-                       foreach ( $this->mServers as $i => $conn ) {
-                               if ($i == 0) { # Master
-                                       $times[$i] = 0;
-                               } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
-                                       $times[$i] = $conn->getLag();
-                               } elseif ( false !== ( $conn = $this->openConnection( $i, $wiki ) ) ) {
-                                       $times[$i] = $conn->getLag();
-                               }
-                       }
-
-                       # Add a timestamp key so we know when it was cached
-                       $times['timestamp'] = time();
-                       $wgMemc->set( $memcKey, $times, $expiry );
-
-                       # But don't give the timestamp to the caller
-                       unset($times['timestamp']);
-                       $this->mLagTimes = $times;
+               # Try process cache
+               if ( isset( $this->mLagTimes ) ) {
+                       return $this->mLagTimes;
                }
-               wfProfileOut( __METHOD__ );
+               # No, send the request to the load monitor
+               $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
                return $this->mLagTimes;
        }
 }
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
new file mode 100644 (file)
index 0000000..8e16f1a
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * An interface for database load monitoring
+ */
+
+interface LoadMonitor {
+       /**
+        * Construct a new LoadMonitor with a given LoadBalancer parent
+        */
+       function __construct( $parent );
+       
+       /**
+        * Perform pre-connection load ratio adjustment.
+        * @param array $loads
+        * @param string $group The selected query group
+        * @param string $wiki
+        */
+       function scaleLoads( &$loads, $group = false, $wiki = false );
+
+       /**
+        * Perform post-connection backoff.
+        *
+        * If the connection is in overload, this should return a backoff factor 
+        * which will be used to control polling time. The number of threads 
+        * connected is a good measure.
+        *
+        * If there is no overload, zero can be returned.
+        *
+        * A threshold thread count is given, the concrete class may compare this 
+        * to the running thread count. The threshold may be false, which indicates
+        * that the sysadmin has not configured this feature.
+        *
+        * @param Database $conn
+        * @param float $threshold
+        */
+       function postConnectionBackoff( $conn, $threshold );
+
+       /**
+        * Return an estimate of replication lag for each server
+        */
+       function getLagTimes( $serverIndexes, $wiki );
+}
+
+
+/**
+ * Basic MySQL load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
+ */
+
+class LoadMonitor_MySQL implements LoadMonitor {
+       var $parent; // LoadBalancer
+
+       function __construct( $parent ) {
+               $this->parent = $parent;
+       }
+
+       function scaleLoads( &$loads, $group = false, $wiki = false ) {
+       }
+
+       function getLagTimes( $serverIndexes, $wiki ) {
+               wfProfileIn( __METHOD__ );
+               $expiry = 5;
+               $requestRate = 10;
+
+               global $wgMemc;
+               $masterName = $this->parent->getServerName( 0 );
+               $memcKey = wfMemcKey( 'lag_times', $masterName );
+               $times = $wgMemc->get( $memcKey );
+               if ( $times ) {
+                       # Randomly recache with probability rising over $expiry
+                       $elapsed = time() - $times['timestamp'];
+                       $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+                       if ( mt_rand( 0, $chance ) != 0 ) {
+                               unset( $times['timestamp'] );
+                               wfProfileOut( __METHOD__ );
+                               return $times;
+                       }
+                       wfIncrStats( 'lag_cache_miss_expired' );
+               } else {
+                       wfIncrStats( 'lag_cache_miss_absent' );
+               }
+
+               # Cache key missing or expired
+
+               $times = array();
+               foreach ( $serverIndexes as $i ) {
+                       if ($i == 0) { # Master
+                               $times[$i] = 0;
+                       } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) {
+                               $times[$i] = $conn->getLag();
+                       } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) {
+                               $times[$i] = $conn->getLag();
+                       }
+               }
+
+               # Add a timestamp key so we know when it was cached
+               $times['timestamp'] = time();
+               $wgMemc->set( $memcKey, $times, $expiry );
+
+               # But don't give the timestamp to the caller
+               unset($times['timestamp']);
+               $lagTimes = $times;
+
+               wfProfileOut( __METHOD__ );
+               return $lagTimes;
+       }
+
+       function postConnectionBackoff( $conn, $threshold ) {
+               if ( !$threshold ) {
+                       return 0;
+               }
+               $status = $conn->getStatus("Thread%");
+               if ( $status['Threads_running'] > $threshold ) {
+                       return $status['Threads_connected'];
+               } else {
+                       return 0;
+               }
+       }
+}
+
index f723525..c1c4886 100644 (file)
@@ -133,6 +133,8 @@ class Parser
                $this->mVarCache = array();
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
+               } elseif ( class_exists( 'DOMDocument' ) ) {
+                       $this->mPreprocessorClass = 'Preprocessor_DOM';
                } else {
                        $this->mPreprocessorClass = 'Preprocessor_Hash';
                }
index d74e32b..99e69ce 100644 (file)
@@ -97,6 +97,7 @@ class BackupReader {
                        }
                        $this->progress( "$this->pageCount ($rate pages/sec $revrate revs/sec)" );
                }
+               wfWaitForSlaves(5);
        }
 
        function progress( $string ) {
index 1e1c483..d1ac875 100644 (file)
@@ -47,6 +47,16 @@ class ParserTest {
         */
        private $showOutput;
 
+       /**
+        * boolean $useTemporaryTables Use temporary tables for the temporary database
+        */
+       private $useTemporaryTables = true;
+
+       /**
+        * boolean $databaseSetupDone True if the database has been set up
+        */
+       private $databaseSetupDone = false;
+
        /**
         * Sets terminal colorization and diff/quick modes depending on OS and
         * command-line options (--color and --quick).
@@ -126,6 +136,7 @@ class ParserTest {
         * @return bool True if passed all tests, false if any tests failed.
         */
        public function runTestsFromFiles( $filenames ) {
+               $this->setupDatabase();
                $this->recorder->start();
                $ok = true;
                foreach( $filenames as $filename ) {
@@ -133,6 +144,7 @@ class ParserTest {
                }
                $this->recorder->report();
                $this->recorder->end();
+               $this->teardownDatabase();
                return $ok;
        }
 
@@ -351,12 +363,6 @@ class ParserTest {
         * Ideally this should replace the global configuration entirely.
         */
        private function setupGlobals($opts = '') {
-               # Save the prefixed / quoted table names for later use when we make the temporaries.
-               $db = wfGetDB( DB_SLAVE );
-               $this->oldTableNames = array();
-               foreach( $this->listTables() as $table ) {
-                       $this->oldTableNames[$table] = $db->tableName( $table );
-               }
                if( !isset( $this->uploadDir ) ) {
                        $this->uploadDir = $this->setupUploadDir();
                }
@@ -424,7 +430,6 @@ class ParserTest {
                $GLOBALS['wgContLang'] = $langObj;
 
                //$GLOBALS['wgMessageCache'] = new MessageCache( new BagOStuff(), false, 0, $GLOBALS['wgDBname'] );
-               $this->setupDatabase();
 
                global $wgUser;
                $wgUser = new User();
@@ -462,97 +467,139 @@ class ParserTest {
         * the db will be visible to later tests in the run.
         */
        private function setupDatabase() {
-               static $setupDB = false;
                global $wgDBprefix;
+               if ( $this->databaseSetupDone ) {
+                       return;
+               }
+               if ( $wgDBprefix === 'parsertest_' ) {
+                       throw new MWException( 'setupDatabase should be called before setupGlobals' );
+               }
+               $this->databaseSetupDone = true;
 
-               # Make sure we don't mess with the live DB
-               if (!$setupDB && $wgDBprefix === 'parsertest_') {
-                       # oh teh horror
-                       LBFactory::destroy();
-                       $db = wfGetDB( DB_MASTER );
-
-                       $tables = $this->listTables();
-
-                       if (!(strcmp($db->getServerVersion(), '4.1') < 0 and stristr($db->getSoftwareLink(), 'MySQL'))) {
-                               # Database that supports CREATE TABLE ... LIKE
-                               global $wgDBtype;
-                               if( $wgDBtype == 'postgres' ) {
-                                       $def = 'INCLUDING DEFAULTS';
-                               } else {
-                                       $def = '';
-                               }
-                               foreach ($tables as $tbl) {
-                                       $newTableName = $db->tableName( $tbl );
-                                       $tableName = $this->oldTableNames[$tbl];
-                                       $db->query("CREATE TEMPORARY TABLE $newTableName (LIKE $tableName $def)");
-                               }
+               # CREATE TEMPORARY TABLE breaks if there is more than one server
+               if ( wfGetLB()->getServerCount() != 1 ) {
+                       $this->useTemporaryTables = false;
+               }
+
+               $temporary = $this->useTemporaryTables ? 'TEMPORARY' : '';
+
+               $db = wfGetDB( DB_MASTER );
+               $tables = $this->listTables();
+
+               if (!(strcmp($db->getServerVersion(), '4.1') < 0 and stristr($db->getSoftwareLink(), 'MySQL'))) {
+                       # Database that supports CREATE TABLE ... LIKE
+                       global $wgDBtype;
+                       if( $wgDBtype == 'postgres' ) {
+                               $def = 'INCLUDING DEFAULTS';
                        } else {
-                               # Hack for MySQL versions < 4.1, which don't support
-                               # "CREATE TABLE ... LIKE". Note that
-                               # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0"
-                               # would not create the indexes we need....
-                               foreach ($tables as $tbl) {
-                                       $res = $db->query("SHOW CREATE TABLE {$this->oldTableNames[$tbl]}");
-                                       $row = $db->fetchRow($res);
-                                       $create = $row[1];
-                                       $create_tmp = preg_replace('/CREATE TABLE `(.*?)`/', 'CREATE TEMPORARY TABLE `'
-                                               . $wgDBprefix . $tbl .'`', $create);
-                                       if ($create === $create_tmp) {
-                                               # Couldn't do replacement
-                                               wfDie("could not create temporary table $tbl");
-                                       }
-                                       $db->query($create_tmp);
+                               $def = '';
+                       }
+                       foreach ($tables as $tbl) {
+                               $oldTableName = $db->tableName( $tbl );
+                               # Clean up from previous aborted run
+                               if ( $db->tableExists( "`parsertest_$tbl`" ) ) {
+                                       $db->query("DROP TABLE `parsertest_$tbl`");
                                }
-
+                               # Create new table
+                               $db->query("CREATE $temporary TABLE `parsertest_$tbl` (LIKE $oldTableName $def)");
+                       }
+               } else {
+                       # Hack for MySQL versions < 4.1, which don't support
+                       # "CREATE TABLE ... LIKE". Note that
+                       # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0"
+                       # would not create the indexes we need....
+                       foreach ($tables as $tbl) {
+                               $oldTableName = $db->tableName( $tbl );
+                               $res = $db->query("SHOW CREATE TABLE $oldTableName");
+                               $row = $db->fetchRow($res);
+                               $create = $row[1];
+                               $create_tmp = preg_replace('/CREATE TABLE `(.*?)`/', 
+                                       "CREATE $temporary TABLE `parsertest_$tbl`", $create);
+                               if ($create === $create_tmp) {
+                                       # Couldn't do replacement
+                                       wfDie("could not create temporary table $tbl");
+                               }
+                               $db->query($create_tmp);
                        }
-
-                       # Hack: insert a few Wikipedia in-project interwiki prefixes,
-                       # for testing inter-language links
-                       $db->insert( 'interwiki', array(
-                               array( 'iw_prefix' => 'Wikipedia',
-                                      'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
-                                      'iw_local'  => 0 ),
-                               array( 'iw_prefix' => 'MeatBall',
-                                      'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
-                                      'iw_local'  => 0 ),
-                               array( 'iw_prefix' => 'zh',
-                                      'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
-                                      'iw_local'  => 1 ),
-                               array( 'iw_prefix' => 'es',
-                                      'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
-                                      'iw_local'  => 1 ),
-                               array( 'iw_prefix' => 'fr',
-                                      'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
-                                      'iw_local'  => 1 ),
-                               array( 'iw_prefix' => 'ru',
-                                      'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
-                                      'iw_local'  => 1 ),
-                               ) );
-
-                       # Hack: Insert an image to work with
-                       $db->insert( 'image', array(
-                               'img_name'        => 'Foobar.jpg',
-                               'img_size'        => 12345,
-                               'img_description' => 'Some lame file',
-                               'img_user'        => 1,
-                               'img_user_text'   => 'WikiSysop',
-                               'img_timestamp'   => $db->timestamp( '20010115123500' ),
-                               'img_width'       => 1941,
-                               'img_height'      => 220,
-                               'img_bits'        => 24,
-                               'img_media_type'  => MEDIATYPE_BITMAP,
-                               'img_major_mime'  => "image",
-                               'img_minor_mime'  => "jpeg",
-                               'img_metadata'    => serialize( array() ),
-                               ) );
-
-                       # Update certain things in site_stats
-                       $db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 1, 'ss_good_articles' => 1 ) );
-
-                       $setupDB = true;
                }
+
+               # Hack: insert a few Wikipedia in-project interwiki prefixes,
+               # for testing inter-language links
+               $db->insert( '`parsertest_interwiki`', array(
+                       array( 'iw_prefix' => 'Wikipedia',
+                                  'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
+                                  'iw_local'  => 0 ),
+                       array( 'iw_prefix' => 'MeatBall',
+                                  'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
+                                  'iw_local'  => 0 ),
+                       array( 'iw_prefix' => 'zh',
+                                  'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
+                                  'iw_local'  => 1 ),
+                       array( 'iw_prefix' => 'es',
+                                  'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
+                                  'iw_local'  => 1 ),
+                       array( 'iw_prefix' => 'fr',
+                                  'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
+                                  'iw_local'  => 1 ),
+                       array( 'iw_prefix' => 'ru',
+                                  'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
+                                  'iw_local'  => 1 ),
+                       ) );
+
+               # Hack: Insert an image to work with
+               $db->insert( '`parsertest_image`', array(
+                       'img_name'        => 'Foobar.jpg',
+                       'img_size'        => 12345,
+                       'img_description' => 'Some lame file',
+                       'img_user'        => 1,
+                       'img_user_text'   => 'WikiSysop',
+                       'img_timestamp'   => $db->timestamp( '20010115123500' ),
+                       'img_width'       => 1941,
+                       'img_height'      => 220,
+                       'img_bits'        => 24,
+                       'img_media_type'  => MEDIATYPE_BITMAP,
+                       'img_major_mime'  => "image",
+                       'img_minor_mime'  => "jpeg",
+                       'img_metadata'    => serialize( array() ),
+                       ) );
+
+               # Update certain things in site_stats
+               $db->insert( '`parsertest_site_stats`', array( 'ss_row_id' => 1, 'ss_images' => 1, 'ss_good_articles' => 1 ) );
+
+               # Change the table prefixes on all open connections
+               LBFactory::singleton()->forEachLB( array( $this, 'changeLBPrefix' ) );
+               $wgDBprefix = 'parsertest_';
        }
 
+       public function changeLBPrefix( $lb ) {
+               $lb->forEachOpenConnection( array( $this, 'changeDBPrefix' ) );
+       }
+
+       public function changeDBPrefix( $db ) {
+               $db->tablePrefix( 'parsertest_' );
+       }
+
+       private function teardownDatabase() {
+               if ( !$this->databaseSetupDone ) {
+                       return;
+               }
+               $this->databaseSetupDone = false;
+               if ( $this->useTemporaryTables ) {
+                       # Don't need to do anything
+                       return;
+               }
+
+               $tables = $this->listTables();
+               $db = wfGetDB( DB_MASTER );
+               foreach ( $tables as $table ) {
+                       $db->query( "DROP TABLE `parsertest_$table`" );
+               }
+
+               # Close all connections which are open with the parsertest_ prefix, so that setupDatabase() will work
+               LBFactory::singleton()->forEachLBCallMethod( 'closeAll' );
+               LBFactory::destroy();
+       }
+       
        /**
         * Create a dummy uploads directory which will contain a couple
         * of files in order to pass existence tests.