Attached Files |
db_load_balancer.patch [^] (23,887 bytes) 2011-07-05 12:00
[Show Content]
Index: install.php
===================================================================
--- install.php (revision 14395)
+++ install.php (working copy)
@@ -151,6 +151,7 @@
function Init()
{
+ include_once(FULL_PATH . REL_PATH . '/kernel/kbase.php'); // required by kDBConnection class
include_once(FULL_PATH . REL_PATH . '/kernel/utility/multibyte.php'); // emulating multi-byte php extension
require_once(FULL_PATH . REL_PATH . '/install/install_toolkit.php'); // toolkit required for module installations to installator
$this->toolkit = new kInstallToolkit();
@@ -1245,12 +1246,7 @@
}
$this->Conn = new kDBConnection($this->toolkit->getSystemConfig('Database', 'DBType'), Array(&$this, 'DBErrorHandler'));
- $this->Conn->Connect(
- $this->toolkit->getSystemConfig('Database', 'DBHost'),
- $this->toolkit->getSystemConfig('Database', 'DBUser'),
- $this->toolkit->getSystemConfig('Database', 'DBUserPassword'),
- $this->toolkit->getSystemConfig('Database', 'DBName')
- );
+ $this->Conn->setup( $this->toolkit->systemConfig );
// setup toolkit too
$this->toolkit->Conn =& $this->Conn;
Index: kernel/application.php
===================================================================
--- kernel/application.php (revision 14388)
+++ kernel/application.php (working copy)
@@ -307,12 +307,14 @@
}
}
- $this->Conn = new kDBConnection(SQL_TYPE, Array(&$this, 'handleSQLError') );
- $this->Conn->debugMode = $this->isDebugMode();
- $this->Conn->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB);
-
$this->Factory = new kFactory();
$this->registerDefaultClasses();
+
+ // TODO: in v 5.2.0+ makeClass has 2 arguments ($pseudo, $argumuments)
+ $this->Conn =& $this->Application->makeClass( 'kDBLoadBalancer', SQL_TYPE, Array (&$this->Application, 'handleSQLError'));
+// $this->Conn =& $this->Application->makeClass( 'kDBConnection', SQL_TYPE, Array (&$this->Application, 'handleSQLError'));
+ $this->Conn->setup( parse_portal_ini(true) );
+
$this->Phrases = new PhrasesCache();
$this->memoryCache =& $this->Factory->makeClass('Cache');
$this->EventManager =& $this->Factory->makeClass('EventManager');
@@ -708,6 +710,9 @@
$this->registerClass('kTagProcessor', KERNEL_PATH . '/processors/tag_processor.php');
$this->registerClass('kMainTagProcessor', KERNEL_PATH . '/processors/main_processor.php','m_TagProcessor', 'kTagProcessor');
+ $this->registerClass('kDBConnection', KERNEL_PATH . '/db/db_connection.php');
+ $this->registerClass('kDBLoadBalancer', KERNEL_PATH . '/db/db_load_balancer.php');
+
$this->registerClass('kDBList', KERNEL_PATH . '/db/dblist.php');
$this->registerClass('kDBItem', KERNEL_PATH . '/db/dbitem.php');
$this->registerClass('kDBEventHandler', KERNEL_PATH . '/db/db_event_handler.php');
Index: kernel/db/db_connection.php
===================================================================
--- kernel/db/db_connection.php (revision 14347)
+++ kernel/db/db_connection.php (working copy)
@@ -18,16 +18,9 @@
* Multi database connection class
*
*/
- class kDBConnection {
+ class kDBConnection extends kBase {
/**
- * Holds reference to global KernelApplication instance
- * @access public
- * @var kApplication
- */
- var $Application;
-
- /**
* Current database type
*
* @var string
@@ -44,6 +37,13 @@
var $connectionID = null;
/**
+ * Remembers, that database was opened successfully
+ *
+ * @var unknown_type
+ */
+ var $connectionOpened = false;
+
+ /**
* Connection parameters, that were used
*
* @var Array
@@ -51,6 +51,13 @@
var $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');
/**
+ * Index if database server
+ *
+ * @var int
+ */
+ var $serverIndex = 0;
+
+ /**
* Handle of currenty processed recordset
*
* @var resource
@@ -142,9 +149,16 @@
* @return DBConnection
* @access public
*/
- function kDBConnection($dbType, $errorHandler = '')
+ function kDBConnection($dbType, $errorHandler = '', $server_index = 0)
{
+ if ( class_exists('kApplication') ) {
+ // prevents "Fatal Error" on 2nd installation step (when database is empty)
+ parent::kBase();
+ }
+
$this->dbType = $dbType;
+ $this->serverIndex = $server_index;
+
// $this->initMetaFunctions();
if (!$errorHandler) {
$this->errorHandler = Array(&$this, 'handleError');
@@ -154,11 +168,6 @@
}
$this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN);
-
- if (class_exists('kApplication')) {
- // prevents "Fatal Error" on 2nd installation step (when database is empty)
- $this->Application =& kApplication::Instance();
- }
}
/**
@@ -261,6 +270,8 @@
$this->errorCode = $this->connectionID ? $func($this->connectionID) : $func();
if ( !$this->hasError() ) {
+ $this->connectionOpened = true;
+
return true;
}
@@ -270,9 +281,31 @@
trigger_error($error_msg, (defined('IS_INSTALL') && IS_INSTALL) || $retry ? E_USER_WARNING : E_USER_ERROR);
+ $this->connectionOpened = false;
+
return false;
}
+ /**
+ * Setups the connection according given configuration
+ *
+ * @param Array $config
+ * @return bool
+ */
+ function setup($config)
+ {
+ if ( !defined('IS_INSTALL') ) {
+ $this->debugMode = $this->Application->isDebugMode();
+ }
+
+ return $this->Connect(
+ $config['Database']['DBHost'],
+ $config['Database']['DBUser'],
+ $config['Database']['DBUserPassword'],
+ $config['Database']['DBName']
+ );
+ }
+
function ReConnect($force_new = false)
{
$retry_count = 0;
@@ -310,7 +343,7 @@
function showError($sql = '', $key_field = null, $no_debug = false)
{
static $retry_count = 0;
-
+
$func = $this->getMetaFunction('errno');
if (!$this->connectionID) {
@@ -595,7 +628,7 @@
$first_cell = substr($first_cell, 0, 50) . ' ...';
}
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable);
+ $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
$this->nextQueryCachable = false;
}
@@ -607,7 +640,7 @@
else {
// set 2nd checkpoint: begin
if ($profileSQLs) {
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable);
+ $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
$this->nextQueryCachable = false;
}
@@ -828,4 +861,63 @@
{
return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount);
}
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ *
+ * @param string $which
+ * @return Array
+ */
+ function getStatus($which = '%')
+ {
+ $status = Array ();
+ $records = $this->Query('SHOW STATUS LIKE "' . $which . '"');
+
+ foreach ($records as $record) {
+ $status[ $record['Variable_name'] ] = $record['Value'];
+ }
+
+ return $status;
+ }
+
+ /**
+ * Get slave replication lag. It will only work if the DB user has the PROCESS privilege.
+ *
+ * @return int
+ */
+ function getSlaveLag()
+ {
+ // don't use Query, since it will create an array of all server processes
+ $rs = mysql_query('SHOW PROCESSLIST', $this->connectionID);
+
+ $skip_states = Array (
+ 'Waiting for master to send event',
+ 'Connecting to master',
+ 'Queueing master event to the relay log',
+ 'Waiting for master update',
+ 'Requesting binlog dump',
+ );
+
+ // find slave SQL thread
+ while ( $row = mysql_fetch_array($rs) ) {
+ if ( $row['User'] == 'system user' && !in_array($row['State'], $skip_states) ) {
+ // this is it, return the time (except -ve)
+ return $row['Time'] > 0x7fffffff ? false : $row['Time'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Create new instance of object
+ *
+ * @return kDBConnection
+ */
+ function &makeClass($dbType, $errorHandler = '', $server_index = 0)
+ {
+ $object = new kDBConnection($dbType, $errorHandler, $server_index);
+
+ return $object;
+ }
}
\ No newline at end of file
Index: kernel/db/db_load_balancer.php
===================================================================
--- kernel/db/db_load_balancer.php (revision 0)
+++ kernel/db/db_load_balancer.php (revision 0)
@@ -0,0 +1,544 @@
+<?php
+
+class kDBLoadBalancer extends kBase {
+
+ /**
+ * Current database type
+ *
+ * @var string
+ * @access private
+ */
+ var $dbType = 'mysql';
+
+ /**
+ * Function to handle sql errors
+ *
+ * @var string
+ * @access private
+ */
+ var $errorHandler = '';
+
+ /**
+ * Database connection settings
+ *
+ * @var Array
+ */
+ protected $servers = Array ();
+
+ /**
+ * Load of each slave server, given
+ *
+ * @var Array
+ */
+ protected $serverLoads = Array ();
+
+ /**
+ * Caches replication lag of servers
+ *
+ * @var Array
+ */
+ protected $serverLagTimes = Array ();
+
+ /**
+ * Connection references to opened connections
+ *
+ * @var Array
+ */
+ protected $connections = Array ();
+
+ /**
+ * Index of last user slave connection
+ *
+ * @var int
+ */
+ protected $slaveIndex = false;
+
+ /**
+ * Index of the server, that was used last
+ *
+ * @var int
+ */
+ protected $lastUsedIndex = 0;
+
+ /**
+ * Consider slave down if it isn't responding for N miliseconds
+ *
+ * @var int
+ */
+ protected $DBClusterTimeout = 10;
+
+ /**
+ * Scale load balancer polling time so that under overload conditions, the database server
+ * receives a SHOW STATUS query at an average interval of this many microseconds
+ */
+ protected $DBAvgStatusPoll = 2000;
+
+ /**
+ * Indicates, that next query should be sent to maser database
+ *
+ * @var bool
+ */
+ public $nextQueryFromMaster = false;
+
+ function __construct($dbType, $errorHandler = '')
+ {
+ parent::kBase();
+
+ $this->dbType = $dbType;
+ $this->errorHandler = $errorHandler;
+
+ $this->DBClusterTimeout *= 1e6; // convert to miliseconds
+ }
+
+ /**
+ * Setups load balancer according given configuration
+ *
+ * @param Array $config
+ * @return bool
+ */
+ function setup($config)
+ {
+ $this->servers = Array ();
+
+ $this->servers[0] = Array (
+ 'DBHost' => $config['Database']['DBHost'],
+ 'DBUser' => $config['Database']['DBUser'],
+ 'DBUserPassword' => $config['Database']['DBUserPassword'],
+ 'DBName' => $config['Database']['DBName'],
+ 'DBLoad' => 0,
+ );
+
+ if ( isset($config['Databases']) ) {
+ $this->servers = array_merge($this->servers, $config['Databases']);
+ }
+
+ foreach ($this->servers as $server_index => $server_setting) {
+ $this->serverLoads[$server_index] = $server_setting['DBLoad'];
+ }
+ }
+
+ /**
+ * Returns connection index to master database
+ *
+ * @return int
+ */
+ function getMasterIndex()
+ {
+ return 0;
+ }
+
+ /**
+ * Returns connection index to slave database. This takes into account load ratios and lag times.
+ * Side effect: opens connections to databases
+ *
+ * @return int
+ */
+ function getSlaveIndex()
+ {
+ if ( count($this->servers) == 1 || $this->Application->isAdmin ) {
+ // skip the load balancing if there's only one server OR in admin console
+ return 0;
+ }
+ elseif ( $this->slaveIndex !== false ) {
+ // shortcut if generic reader exists already
+ return $this->slaveIndex;
+ }
+
+ $total_elapsed = 0;
+ $non_error_loads = $this->serverLoads;
+ $i = $found = $lagged_slave_mode = false;
+
+ // first try quickly looking through the available servers for a server that meets our criteria
+ do {
+ $current_loads = $non_error_loads;
+ $overloaded_servers = $total_threads_connected = 0;
+
+ while ($current_loads) {
+ if ( $lagged_slave_mode ) {
+ // when all slave servers are too lagged, then ignore lag and pick random server
+ $i = $this->pickRandom($current_loads);
+ }
+ else {
+ $i = $this->getRandomNonLagged($current_loads);
+
+ if ( $i === false && $current_loads ) {
+ // all slaves lagged -> pick random lagged slave then
+ $lagged_slave_mode = true;
+ $i = $this->pickRandom( $current_loads );
+ }
+ }
+
+ if ( $i === false ) {
+ // all slaves are down -> use master as a slave
+ return $this->getMasterIndex();
+ }
+
+ $conn =& $this->openConnection($i);
+
+ if ( !$conn ) {
+ unset($non_error_loads[$i], $current_loads[$i]);
+ continue;
+ }
+
+ // Perform post-connection backoff
+ $threshold = isset($this->servers[$i]['DBMaxThreads']) ? $this->servers[$i]['DBMaxThreads'] : false;
+ $backoff = $this->postConnectionBackoff($conn, $threshold);
+
+ if ( $backoff ) {
+ // post-connection overload, don't use this server for now
+ $total_threads_connected += $backoff;
+ $overloaded_servers++;
+
+ unset( $current_loads[$i] );
+ }
+ else {
+ // return this server
+ break 2;
+ }
+ }
+
+ // no server found yet
+ $i = false;
+
+ // if all servers were down, quit now
+ if ( !$non_error_loads ) {
+ break;
+ }
+
+ // back off for a while
+ // scale the sleep time by the number of connected threads, to produce a roughly constant global poll rate
+ $avg_threads = $total_threads_connected / $overloaded_servers;
+
+ usleep($this->DBAvgStatusPoll * $avg_threads);
+ $total_elapsed += $this->DBAvgStatusPoll * $avg_threads;
+ } while ( $total_elapsed < $this->DBClusterTimeout );
+
+ if ( $i !== false ) {
+ // slave connection successful
+ if ( $this->slaveIndex <= 0 && $this->serverLoads[$i] > 0 && $i !== false ) {
+ $this->slaveIndex = $i;
+ }
+ }
+
+ return $i;
+ }
+
+ /**
+ * Returns random non-lagged server
+ *
+ * @param Array $loads
+ * @return int
+ */
+ function getRandomNonLagged($loads)
+ {
+ // unset excessively lagged servers
+ $lags = $this->getLagTimes();
+
+ foreach ($lags as $i => $lag) {
+ if ( $i != 0 && isset($this->servers[$i]['DBMaxLag']) ) {
+ if ( $lag === false ) {
+ unset( $loads[$i] ); // server is not replicating
+ }
+ elseif ( $lag > $this->servers[$i]['DBMaxLag'] ) {
+ unset( $loads[$i] ); // server is excessively lagged
+ }
+ }
+ }
+
+ // find out if all the slaves with non-zero load are lagged
+ if ( !$loads || array_sum($loads) == 0 ) {
+ return false;
+ }
+
+ // return a random representative of the remainder
+ return $this->pickRandom($loads);
+ }
+
+ /**
+ * Select an element from an array of non-normalised probabilities
+ *
+ * @param Array $weights
+ * @return int
+ */
+ function pickRandom($weights)
+ {
+ if ( !is_array($weights) || !$weights ) {
+ return false;
+ }
+
+ $sum = array_sum($weights);
+
+ if ( $sum == 0 ) {
+ return false;
+ }
+
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $sum = 0;
+
+ foreach ($weights as $index => $weight) {
+ $sum += $weight;
+
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+
+ return $index;
+ }
+
+ /**
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
+ */
+ function getLagTimes( $wiki = false )
+ {
+ if ( $this->serverLagTimes ) {
+ return $this->serverLagTimes;
+ }
+
+ $expiry = 5;
+ $request_rate = 10;
+
+ $cache_key = 'lag_times:' . $this->servers[0]['DBHost'];
+ $times = $this->Application->getCache($cache_key);
+
+ if ( $times ) {
+ // randomly recache with probability rising over $expiry
+ $elapsed = adodb_mktime() - $times['timestamp'];
+ $chance = max(0, ($expiry - $elapsed) * $request_rate);
+
+ if ( mt_rand(0, $chance) != 0 ) {
+ unset( $times['timestamp'] );
+ $this->serverLagTimes = $times;
+
+ return $times;
+ }
+ }
+
+ // cache key missing or expired
+ $times = Array();
+
+ foreach ($this->servers as $index => $server) {
+ if ($index == 0) {
+ $times[$index] = 0; // master
+ }
+ else {
+ $conn =& $this->openConnection($index);
+
+ if ($conn !== false) {
+ $times[$index] = $conn->getSlaveLag();
+ }
+ }
+ }
+
+ // add a timestamp key so we know when it was cached
+ $times['timestamp'] = adodb_mktime();
+ $this->Application->setCache($cache_key, $times, $expiry);
+
+ // but don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $this->serverLagTimes = $times;
+
+ return $this->serverLagTimes;
+ }
+
+ /**
+ * Determines whatever server should not be used, even, when connection was made
+ *
+ * @param kDBConnection $conn
+ * @param int $threshold
+ * @return int
+ */
+ function postConnectionBackoff(&$conn, $threshold)
+ {
+ if ( !$threshold ) {
+ return 0;
+ }
+
+ $status = $conn->getStatus('Thread%');
+
+ return $status['Threads_running'] > $threshold ? $status['Threads_connected'] : 0;
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false.
+ *
+ * @param integer $i Server index
+ * @return kDBConnection
+ */
+ function &openConnection($i)
+ {
+ if ( isset($this->connections[$i]) ) {
+ $conn =& $this->connections[$i];
+ }
+ else {
+ $server = $this->servers[$i];
+ $server['serverIndex'] = $i;
+ $conn =& $this->reallyOpenConnection($server);
+
+ if ( $conn->connectionOpened ) {
+ $this->connections[$i] =&$conn;
+ $this->lastUsedIndex = $i;
+ }
+ else {
+ $conn = false;
+ }
+ }
+
+ return $conn;
+ }
+
+ /**
+ * Really opens a connection.
+ * Returns a database object whether or not the connection was successful.
+ *
+ * @return kDBConnection
+ */
+ function &reallyOpenConnection($server)
+ {
+ // TODO: in v 5.2.0+ makeClass has 2 arguments ($pseudo, $argumuments)
+ $db =& $this->Application->makeClass( 'kDBConnection', $this->dbType, $this->errorHandler, $server['serverIndex'] );
+
+ $db->debugMode = $this->Application->isDebugMode();
+ $db->Connect($server['DBHost'], $server['DBUser'], $server['DBUserPassword'], $this->servers[0]['DBName'], true, true);
+
+ return $db;
+ }
+
+
+
+ /**
+ * Returns first field of first line of recordset if query ok or false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return string
+ */
+ function GetOne($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetOne($sql, $offset);
+ }
+
+ /**
+ * Returns first row of recordset if query ok, false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return Array
+ */
+ function GetRow($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetRow($sql, $offset);
+ }
+
+ /**
+ * Returns 1st column of recordset as one-dimensional array or false otherwise
+ * Optional parameter $key_field can be used to set field name to be used as resulting array key
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ */
+ function GetCol($sql, $key_field = null)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetCol($sql, $key_field);
+ }
+
+ /**
+ * Queries db with $sql query supplied and returns rows selected if any, false
+ * otherwise. Optional parameter $key_field allows to set one of the query fields
+ * value as key in string array.
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ */
+ function Query($sql, $key_field = null, $no_debug = false)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->Query($sql, $key_field, $no_debug);
+ }
+
+ function ChangeQuery($sql)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->ChangeQuery($sql);
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0)
+ * Otherwise returns as-is
+ *
+ * @param mixed $string
+ *
+ * @return string
+ */
+ function qstr($string)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return $conn->qstr($string);
+ }
+
+ public function __call($name, $arguments)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return call_user_func_array( Array (&$conn, $name), $arguments );
+ }
+
+ /**
+ * Returns appropriate connection based on sql
+ *
+ * @param string $sql
+ * @return kDBConnection
+ */
+ function &chooseConnection($sql)
+ {
+ if ( $this->nextQueryFromMaster ) {
+ $this->nextQueryFromMaster = false;
+ $index = $this->getMasterIndex();
+ }
+
+ $sid = isset($this->Application->Session) ? $this->Application->GetSID() : '9999999999999999999999';
+
+ if ( preg_match('/(^[ \t\r\n]*(ALTER|CREATE|DROP|RENAME|DELETE|DO|INSERT|LOAD|REPLACE|TRUNCATE|UPDATE))|ses_' . $sid . '/', $sql) ) {
+ $index = $this->getMasterIndex();
+ }
+ else {
+ $index = $this->getSlaveIndex();
+ }
+
+ $this->lastUsedIndex = $index;
+ $conn =& $this->openConnection($index);
+
+ return $conn;
+ }
+
+
+ /**
+ * Create new instance of object
+ *
+ * @return kDBLoadBalancer
+ */
+ function &makeClass($dbType, $errorHandler = '')
+ {
+ $object = new kDBLoadBalancer($dbType, $errorHandler);
+
+ return $object;
+ }
+}
\ No newline at end of file
Index: kernel/globals.php
===================================================================
--- kernel/globals.php (revision 14325)
+++ kernel/globals.php (working copy)
@@ -156,6 +156,10 @@
require($file);
if ($parse_section) {
+ if ( isset($_CONFIG['Database']['LoadBalancing']) && $_CONFIG['Database']['LoadBalancing'] ) {
+ require FULL_PATH . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR . 'db_servers.php';
+ }
+
return $_CONFIG;
}
Index: kernel/startup.php
===================================================================
--- kernel/startup.php (revision 14360)
+++ kernel/startup.php (working copy)
@@ -165,8 +165,9 @@
$includes = Array(
KERNEL_PATH . '/application.php',
FULL_PATH . APPLICATION_PATH,
+ KERNEL_PATH . "/kbase.php",
KERNEL_PATH . '/db/db_connection.php',
- KERNEL_PATH . "/kbase.php",
+ KERNEL_PATH . '/db/db_load_balancer.php',
KERNEL_PATH . '/utility/event.php',
KERNEL_PATH . "/utility/factory.php",
KERNEL_PATH . "/languages/phrases_cache.php",
Index: kernel/utility/debugger.php
===================================================================
--- kernel/utility/debugger.php (revision 14360)
+++ kernel/utility/debugger.php (working copy)
@@ -896,8 +896,14 @@
$trace_count = count($trace_results);
$i = 0;
while ($i < $trace_count) {
+ if ( !isset($trace_results[$i]['file']) ) {
+ $i++;
+ continue;
+ }
+
$trace_file = basename($trace_results[$i]['file']);
- if ($trace_file != 'db_connection.php' && $trace_file != 'adodb.inc.php') {
+
+ if ($trace_file != 'db_connection.php' && $trace_file != 'db_load_balancer.php' && $trace_file != 'adodb.inc.php') {
break;
}
$i++;
@@ -954,7 +960,11 @@
$this->ProfilerData[$key]['subtitle'] = 'cachable';
}
- if (array_key_exists('prefix_special', $this->ProfilerData[$key])) {
+ if ($func_arguments[7]) {
+ $additional[] = Array ('name' => 'Server #', 'value' => $func_arguments[7]);
+ }
+
+ if ( isset($this->ProfilerData[$key]['prefix_special']) && $this->ProfilerData[$key]['prefix_special'] ) {
$additional[] = Array ('name' => 'PrefixSpecial', 'value' => $this->ProfilerData[$key]['prefix_special']);
}
db_load_balancer_v2.patch [^] (25,715 bytes) 2011-07-29 03:32
[Show Content]
Index: install.php
===================================================================
--- install.php (revision 14467)
+++ install.php (working copy)
@@ -151,6 +151,7 @@
function Init()
{
+ include_once(FULL_PATH . REL_PATH . '/kernel/kbase.php'); // required by kDBConnection class
include_once(FULL_PATH . REL_PATH . '/kernel/utility/multibyte.php'); // emulating multi-byte php extension
require_once(FULL_PATH . REL_PATH . '/install/install_toolkit.php'); // toolkit required for module installations to installator
$this->toolkit = new kInstallToolkit();
@@ -1245,12 +1246,7 @@
}
$this->Conn = new kDBConnection($this->toolkit->getSystemConfig('Database', 'DBType'), Array(&$this, 'DBErrorHandler'));
- $this->Conn->Connect(
- $this->toolkit->getSystemConfig('Database', 'DBHost'),
- $this->toolkit->getSystemConfig('Database', 'DBUser'),
- $this->toolkit->getSystemConfig('Database', 'DBUserPassword'),
- $this->toolkit->getSystemConfig('Database', 'DBName')
- );
+ $this->Conn->setup( $this->toolkit->systemConfig );
// setup toolkit too
$this->toolkit->Conn =& $this->Conn;
Index: kernel/application.php
===================================================================
--- kernel/application.php (revision 14467)
+++ kernel/application.php (working copy)
@@ -307,12 +307,14 @@
}
}
- $this->Conn = new kDBConnection(SQL_TYPE, Array(&$this, 'handleSQLError') );
- $this->Conn->debugMode = $this->isDebugMode();
- $this->Conn->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB);
-
$this->Factory = new kFactory();
$this->registerDefaultClasses();
+
+ // TODO: in v 5.2.0+ makeClass has 2 arguments ($pseudo, $argumuments)
+ $this->Conn =& $this->Application->makeClass( 'kDBLoadBalancer', SQL_TYPE, Array (&$this->Application, 'handleSQLError'));
+// $this->Conn =& $this->Application->makeClass( 'kDBConnection', SQL_TYPE, Array (&$this->Application, 'handleSQLError'));
+ $this->Conn->setup( parse_portal_ini(true) );
+
$this->Phrases = new PhrasesCache();
$this->memoryCache =& $this->Factory->makeClass('Cache');
$this->EventManager =& $this->Factory->makeClass('EventManager');
@@ -708,6 +710,9 @@
$this->registerClass('kTagProcessor', KERNEL_PATH . '/processors/tag_processor.php');
$this->registerClass('kMainTagProcessor', KERNEL_PATH . '/processors/main_processor.php','m_TagProcessor', 'kTagProcessor');
+ $this->registerClass('kDBConnection', KERNEL_PATH . '/db/db_connection.php');
+ $this->registerClass('kDBLoadBalancer', KERNEL_PATH . '/db/db_load_balancer.php');
+
$this->registerClass('kDBList', KERNEL_PATH . '/db/dblist.php');
$this->registerClass('kDBItem', KERNEL_PATH . '/db/dbitem.php');
$this->registerClass('kDBEventHandler', KERNEL_PATH . '/db/db_event_handler.php');
Index: kernel/db/db_connection.php
===================================================================
--- kernel/db/db_connection.php (revision 14467)
+++ kernel/db/db_connection.php (working copy)
@@ -18,16 +18,9 @@
* Multi database connection class
*
*/
- class kDBConnection {
+ class kDBConnection extends kBase {
/**
- * Holds reference to global KernelApplication instance
- * @access public
- * @var kApplication
- */
- var $Application;
-
- /**
* Current database type
*
* @var string
@@ -44,6 +37,13 @@
var $connectionID = null;
/**
+ * Remembers, that database connection was opened successfully
+ *
+ * @var unknown_type
+ */
+ var $connectionOpened = false;
+
+ /**
* Connection parameters, that were used
*
* @var Array
@@ -51,6 +51,13 @@
var $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');
/**
+ * Index of database server
+ *
+ * @var int
+ */
+ var $serverIndex = 0;
+
+ /**
* Handle of currenty processed recordset
*
* @var resource
@@ -135,6 +142,13 @@
var $nextQueryCachable = false;
/**
+ * For backwards compatibility with kDBLoadBalancer class
+ *
+ * @var bool
+ */
+ public $nextQueryFromMaster = false;
+
+ /**
* Initializes connection class with
* db type to used in future
*
@@ -142,9 +156,16 @@
* @return DBConnection
* @access public
*/
- function kDBConnection($dbType, $errorHandler = '')
+ function kDBConnection($dbType, $errorHandler = '', $server_index = 0)
{
+ if ( class_exists('kApplication') ) {
+ // prevents "Fatal Error" on 2nd installation step (when database is empty)
+ parent::kBase();
+ }
+
$this->dbType = $dbType;
+ $this->serverIndex = $server_index;
+
// $this->initMetaFunctions();
if (!$errorHandler) {
$this->errorHandler = Array(&$this, 'handleError');
@@ -154,11 +175,6 @@
}
$this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN);
-
- if (class_exists('kApplication')) {
- // prevents "Fatal Error" on 2nd installation step (when database is empty)
- $this->Application =& kApplication::Instance();
- }
}
/**
@@ -260,7 +276,9 @@
$func = $this->getMetaFunction('errno');
$this->errorCode = $this->connectionID ? $func($this->connectionID) : $func();
- if ( !$this->hasError() ) {
+ if ( is_resource($this->connectionID) && !$this->hasError() ) {
+ $this->connectionOpened = true;
+
return true;
}
@@ -270,9 +288,31 @@
trigger_error($error_msg, (defined('IS_INSTALL') && IS_INSTALL) || $retry ? E_USER_WARNING : E_USER_ERROR);
+ $this->connectionOpened = false;
+
return false;
}
+ /**
+ * Setups the connection according given configuration
+ *
+ * @param Array $config
+ * @return bool
+ */
+ function setup($config)
+ {
+ if ( !defined('IS_INSTALL') ) {
+ $this->debugMode = $this->Application->isDebugMode();
+ }
+
+ return $this->Connect(
+ $config['Database']['DBHost'],
+ $config['Database']['DBUser'],
+ $config['Database']['DBUserPassword'],
+ $config['Database']['DBName']
+ );
+ }
+
function ReConnect($force_new = false)
{
$retry_count = 0;
@@ -310,7 +350,7 @@
function showError($sql = '', $key_field = null, $no_debug = false)
{
static $retry_count = 0;
-
+
$func = $this->getMetaFunction('errno');
if (!$this->connectionID) {
@@ -595,7 +635,7 @@
$first_cell = substr($first_cell, 0, 50) . ' ...';
}
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable);
+ $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
$this->nextQueryCachable = false;
}
@@ -607,7 +647,7 @@
else {
// set 2nd checkpoint: begin
if ($profileSQLs) {
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable);
+ $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
$debugger->profilerAddTotal('sql', 'sql_'.$queryID);
$this->nextQueryCachable = false;
}
@@ -828,4 +868,63 @@
{
return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount);
}
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ *
+ * @param string $which
+ * @return Array
+ */
+ function getStatus($which = '%')
+ {
+ $status = Array ();
+ $records = $this->Query('SHOW STATUS LIKE "' . $which . '"');
+
+ foreach ($records as $record) {
+ $status[ $record['Variable_name'] ] = $record['Value'];
+ }
+
+ return $status;
+ }
+
+ /**
+ * Get slave replication lag. It will only work if the DB user has the PROCESS privilege.
+ *
+ * @return int
+ */
+ function getSlaveLag()
+ {
+ // don't use kDBConnection::Query method, since it will create an array of all server processes
+ $rs = mysql_query('SHOW PROCESSLIST', $this->connectionID);
+
+ $skip_states = Array (
+ 'Waiting for master to send event',
+ 'Connecting to master',
+ 'Queueing master event to the relay log',
+ 'Waiting for master update',
+ 'Requesting binlog dump',
+ );
+
+ // find slave SQL thread
+ while ( $row = mysql_fetch_array($rs) ) {
+ if ( $row['User'] == 'system user' && !in_array($row['State'], $skip_states) ) {
+ // this is it, return the time (except -ve)
+ return $row['Time'] > 0x7fffffff ? false : $row['Time'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Create new instance of object
+ *
+ * @return kDBConnection
+ */
+ function &makeClass($dbType, $errorHandler = '', $server_index = 0)
+ {
+ $object = new kDBConnection($dbType, $errorHandler, $server_index);
+
+ return $object;
+ }
}
\ No newline at end of file
Index: kernel/db/db_load_balancer.php
===================================================================
--- kernel/db/db_load_balancer.php (revision 0)
+++ kernel/db/db_load_balancer.php (revision 0)
@@ -0,0 +1,569 @@
+<?php
+
+class kDBLoadBalancer extends kBase {
+
+ /**
+ * Current database type
+ *
+ * @var string
+ * @access private
+ */
+ var $dbType = 'mysql';
+
+ /**
+ * Function to handle sql errors
+ *
+ * @var string
+ * @access private
+ */
+ var $errorHandler = '';
+
+ /**
+ * Database connection settings
+ *
+ * @var Array
+ */
+ protected $servers = Array ();
+
+ /**
+ * Load of each slave server, given
+ *
+ * @var Array
+ */
+ protected $serverLoads = Array ();
+
+ /**
+ * Caches replication lag of servers
+ *
+ * @var Array
+ */
+ protected $serverLagTimes = Array ();
+
+ /**
+ * Connection references to opened connections
+ *
+ * @var Array
+ */
+ protected $connections = Array ();
+
+ /**
+ * Index of last user slave connection
+ *
+ * @var int
+ */
+ protected $slaveIndex = false;
+
+ /**
+ * Index of the server, that was used last
+ *
+ * @var int
+ */
+ protected $lastUsedIndex = 0;
+
+ /**
+ * Consider slave down if it isn't responding for N miliseconds
+ *
+ * @var int
+ */
+ protected $DBClusterTimeout = 10;
+
+ /**
+ * Scale load balancer polling time so that under overload conditions, the database server
+ * receives a SHOW STATUS query at an average interval of this many microseconds
+ */
+ protected $DBAvgStatusPoll = 2000;
+
+ /**
+ * Indicates, that next query should be sent to maser database
+ *
+ * @var bool
+ */
+ public $nextQueryFromMaster = false;
+
+ function __construct($dbType, $errorHandler = '')
+ {
+ parent::kBase();
+
+ $this->dbType = $dbType;
+ $this->errorHandler = $errorHandler;
+
+ $this->DBClusterTimeout *= 1e6; // convert to miliseconds
+ }
+
+ /**
+ * Setups load balancer according given configuration
+ *
+ * @param Array $config
+ * @return bool
+ */
+ function setup($config)
+ {
+ $this->servers = Array ();
+
+ $this->servers[0] = Array (
+ 'DBHost' => $config['Database']['DBHost'],
+ 'DBUser' => $config['Database']['DBUser'],
+ 'DBUserPassword' => $config['Database']['DBUserPassword'],
+ 'DBName' => $config['Database']['DBName'],
+ 'DBLoad' => 0,
+ );
+
+ if ( isset($config['Databases']) ) {
+ $this->servers = array_merge($this->servers, $config['Databases']);
+ }
+
+ foreach ($this->servers as $server_index => $server_setting) {
+ $this->serverLoads[$server_index] = $server_setting['DBLoad'];
+ }
+ }
+
+ /**
+ * Returns connection index to master database
+ *
+ * @return int
+ */
+ function getMasterIndex()
+ {
+ return 0;
+ }
+
+ /**
+ * Returns connection index to slave database. This takes into account load ratios and lag times.
+ * Side effect: opens connections to databases
+ *
+ * @return int
+ */
+ function getSlaveIndex()
+ {
+ if ( count($this->servers) == 1 || $this->Application->isAdmin ) {
+ // skip the load balancing if there's only one server OR in admin console
+ return 0;
+ }
+ elseif ( $this->slaveIndex !== false ) {
+ // shortcut if generic reader exists already
+ return $this->slaveIndex;
+ }
+
+ $total_elapsed = 0;
+ $non_error_loads = $this->serverLoads;
+ $i = $found = $lagged_slave_mode = false;
+
+ // first try quickly looking through the available servers for a server that meets our criteria
+ do {
+ $current_loads = $non_error_loads;
+ $overloaded_servers = $total_threads_connected = 0;
+
+ while ($current_loads) {
+ if ( $lagged_slave_mode ) {
+ // when all slave servers are too lagged, then ignore lag and pick random server
+ $i = $this->pickRandom($current_loads);
+ }
+ else {
+ $i = $this->getRandomNonLagged($current_loads);
+
+ if ( $i === false && $current_loads ) {
+ // all slaves lagged -> pick random lagged slave then
+ $lagged_slave_mode = true;
+ $i = $this->pickRandom( $current_loads );
+ }
+ }
+
+ if ( $i === false ) {
+ // all slaves are down -> use master as a slave
+ $this->slaveIndex = $this->getMasterIndex();
+
+ return $this->slaveIndex;
+ }
+
+ $conn =& $this->openConnection($i);
+
+ if ( !$conn ) {
+ unset($non_error_loads[$i], $current_loads[$i]);
+ continue;
+ }
+
+ // Perform post-connection backoff
+ $threshold = isset($this->servers[$i]['DBMaxThreads']) ? $this->servers[$i]['DBMaxThreads'] : false;
+ $backoff = $this->postConnectionBackoff($conn, $threshold);
+
+ if ( $backoff ) {
+ // post-connection overload, don't use this server for now
+ $total_threads_connected += $backoff;
+ $overloaded_servers++;
+
+ unset( $current_loads[$i] );
+ }
+ else {
+ // return this server
+ break 2;
+ }
+ }
+
+ // no server found yet
+ $i = false;
+
+ // if all servers were down, quit now
+ if ( !$non_error_loads ) {
+ break;
+ }
+
+ // back off for a while
+ // scale the sleep time by the number of connected threads, to produce a roughly constant global poll rate
+ $avg_threads = $total_threads_connected / $overloaded_servers;
+
+ usleep($this->DBAvgStatusPoll * $avg_threads);
+ $total_elapsed += $this->DBAvgStatusPoll * $avg_threads;
+ } while ( $total_elapsed < $this->DBClusterTimeout );
+
+ if ( $i !== false ) {
+ // slave connection successful
+ if ( $this->slaveIndex <= 0 && $this->serverLoads[$i] > 0 && $i !== false ) {
+ $this->slaveIndex = $i;
+ }
+ }
+
+ return $i;
+ }
+
+ /**
+ * Returns random non-lagged server
+ *
+ * @param Array $loads
+ * @return int
+ */
+ function getRandomNonLagged($loads)
+ {
+ // unset excessively lagged servers
+ $lags = $this->getLagTimes();
+
+ foreach ($lags as $i => $lag) {
+ if ( $i != 0 && isset($this->servers[$i]['DBMaxLag']) ) {
+ if ( $lag === false ) {
+ unset( $loads[$i] ); // server is not replicating
+ }
+ elseif ( $lag > $this->servers[$i]['DBMaxLag'] ) {
+ unset( $loads[$i] ); // server is excessively lagged
+ }
+ }
+ }
+
+ // find out if all the slaves with non-zero load are lagged
+ if ( !$loads || array_sum($loads) == 0 ) {
+ return false;
+ }
+
+ // return a random representative of the remainder
+ return $this->pickRandom($loads);
+ }
+
+ /**
+ * Select an element from an array of non-normalised probabilities
+ *
+ * @param Array $weights
+ * @return int
+ */
+ function pickRandom($weights)
+ {
+ if ( !is_array($weights) || !$weights ) {
+ return false;
+ }
+
+ $sum = array_sum($weights);
+
+ if ( $sum == 0 ) {
+ return false;
+ }
+
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $sum = 0;
+
+ foreach ($weights as $index => $weight) {
+ $sum += $weight;
+
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+
+ return $index;
+ }
+
+ /**
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
+ */
+ function getLagTimes( $wiki = false )
+ {
+ if ( $this->serverLagTimes ) {
+ return $this->serverLagTimes;
+ }
+
+ $expiry = 5;
+ $request_rate = 10;
+
+ $cache_key = 'lag_times:' . $this->servers[0]['DBHost'];
+ $times = $this->Application->getCache($cache_key);
+
+ if ( $times ) {
+ // randomly recache with probability rising over $expiry
+ $elapsed = adodb_mktime() - $times['timestamp'];
+ $chance = max(0, ($expiry - $elapsed) * $request_rate);
+
+ if ( mt_rand(0, $chance) != 0 ) {
+ unset( $times['timestamp'] );
+ $this->serverLagTimes = $times;
+
+ return $times;
+ }
+ }
+
+ // cache key missing or expired
+ $times = Array();
+
+ foreach ($this->servers as $index => $server) {
+ if ($index == 0) {
+ $times[$index] = 0; // master
+ }
+ else {
+ $conn =& $this->openConnection($index);
+
+ if ($conn !== false) {
+ $times[$index] = $conn->getSlaveLag();
+ }
+ }
+ }
+
+ // add a timestamp key so we know when it was cached
+ $times['timestamp'] = adodb_mktime();
+ $this->Application->setCache($cache_key, $times, $expiry);
+
+ // but don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $this->serverLagTimes = $times;
+
+ return $this->serverLagTimes;
+ }
+
+ /**
+ * Determines whatever server should not be used, even, when connection was made
+ *
+ * @param kDBConnection $conn
+ * @param int $threshold
+ * @return int
+ */
+ function postConnectionBackoff(&$conn, $threshold)
+ {
+ if ( !$threshold ) {
+ return 0;
+ }
+
+ $status = $conn->getStatus('Thread%');
+
+ return $status['Threads_running'] > $threshold ? $status['Threads_connected'] : 0;
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false.
+ *
+ * @param integer $i Server index
+ * @return kDBConnection
+ */
+ function &openConnection($i)
+ {
+ if ( isset($this->connections[$i]) ) {
+ $conn =& $this->connections[$i];
+ }
+ else {
+ $server = $this->servers[$i];
+ $server['serverIndex'] = $i;
+ $conn =& $this->reallyOpenConnection($server);
+
+ if ( $conn->connectionOpened ) {
+ $this->connections[$i] =&$conn;
+ $this->lastUsedIndex = $i;
+ }
+ else {
+ $conn = false;
+ }
+ }
+
+ return $conn;
+ }
+
+ /**
+ * Really opens a connection.
+ * Returns a database object whether or not the connection was successful.
+ *
+ * @return kDBConnection
+ */
+ function &reallyOpenConnection($server)
+ {
+ // TODO: in v 5.2.0+ makeClass has 2 arguments ($pseudo, $argumuments)
+ $db =& $this->Application->makeClass( 'kDBConnection', $this->dbType, $this->errorHandler, $server['serverIndex'] );
+
+ $db->debugMode = $this->Application->isDebugMode();
+ $db->Connect($server['DBHost'], $server['DBUser'], $server['DBUserPassword'], $this->servers[0]['DBName'], true, true);
+
+ return $db;
+ }
+
+ /**
+ * Returns first field of first line of recordset if query ok or false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return string
+ */
+ function GetOne($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetOne($sql, $offset);
+ }
+
+ /**
+ * Returns first row of recordset if query ok, false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return Array
+ */
+ function GetRow($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetRow($sql, $offset);
+ }
+
+ /**
+ * Returns 1st column of recordset as one-dimensional array or false otherwise
+ * Optional parameter $key_field can be used to set field name to be used as resulting array key
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ */
+ function GetCol($sql, $key_field = null)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetCol($sql, $key_field);
+ }
+
+ /**
+ * Queries db with $sql query supplied and returns rows selected if any, false
+ * otherwise. Optional parameter $key_field allows to set one of the query fields
+ * value as key in string array.
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ */
+ function Query($sql, $key_field = null, $no_debug = false)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->Query($sql, $key_field, $no_debug);
+ }
+
+ function ChangeQuery($sql)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->ChangeQuery($sql);
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0)
+ * Otherwise returns as-is
+ *
+ * @param mixed $string
+ *
+ * @return string
+ */
+ function qstr($string)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return $conn->qstr($string);
+ }
+
+ /**
+ * Performs insert of given data (useful with small number of queries)
+ * or stores it to perform multiple insert later (useful with large number of queries)
+ *
+ * @param Array $fields_hash
+ * @param string $table
+ * @param string $type
+ * @param bool $insert_now
+ * @return bool
+ */
+ function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
+ {
+ $conn =& $this->openConnection( $this->getMasterIndex() );
+
+ return $conn->doInsert($fields_hash, $table, $type, $insert_now);
+ }
+
+ function doUpdate($fields_hash, $table, $key_clause)
+ {
+ $conn =& $this->openConnection( $this->getMasterIndex() );
+
+ return $conn->doUpdate($fields_hash, $table, $key_clause);
+ }
+
+ public function __call($name, $arguments)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return call_user_func_array( Array (&$conn, $name), $arguments );
+ }
+
+ /**
+ * Returns appropriate connection based on sql
+ *
+ * @param string $sql
+ * @return kDBConnection
+ */
+ function &chooseConnection($sql)
+ {
+ if ( $this->nextQueryFromMaster ) {
+ $this->nextQueryFromMaster = false;
+ $index = $this->getMasterIndex();
+ }
+ else {
+ $sid = isset($this->Application->Session) ? $this->Application->GetSID() : '9999999999999999999999';
+
+ if ( preg_match('/(^[ \t\r\n]*(ALTER|CREATE|DROP|RENAME|DELETE|DO|INSERT|LOAD|REPLACE|TRUNCATE|UPDATE))|ses_' . $sid . '/', $sql) ) {
+ $index = $this->getMasterIndex();
+ }
+ else {
+ $index = $this->getSlaveIndex();
+ }
+ }
+
+ $this->lastUsedIndex = $index;
+ $conn =& $this->openConnection($index);
+
+ return $conn;
+ }
+
+
+ /**
+ * Create new instance of object
+ *
+ * @return kDBLoadBalancer
+ */
+ function &makeClass($dbType, $errorHandler = '')
+ {
+ $object = new kDBLoadBalancer($dbType, $errorHandler);
+
+ return $object;
+ }
+}
Property changes on: kernel\db\db_load_balancer.php
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ LF
Index: kernel/globals.php
===================================================================
--- kernel/globals.php (revision 14467)
+++ kernel/globals.php (working copy)
@@ -156,6 +156,10 @@
require($file);
if ($parse_section) {
+ if ( isset($_CONFIG['Database']['LoadBalancing']) && $_CONFIG['Database']['LoadBalancing'] ) {
+ require FULL_PATH . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR . 'db_servers.php';
+ }
+
return $_CONFIG;
}
Index: kernel/startup.php
===================================================================
--- kernel/startup.php (revision 14467)
+++ kernel/startup.php (working copy)
@@ -165,8 +165,9 @@
$includes = Array(
KERNEL_PATH . '/application.php',
FULL_PATH . APPLICATION_PATH,
+ KERNEL_PATH . "/kbase.php",
KERNEL_PATH . '/db/db_connection.php',
- KERNEL_PATH . "/kbase.php",
+ KERNEL_PATH . '/db/db_load_balancer.php',
KERNEL_PATH . '/utility/event.php',
KERNEL_PATH . "/utility/factory.php",
KERNEL_PATH . "/languages/phrases_cache.php",
Index: kernel/utility/cache.php
===================================================================
--- kernel/utility/cache.php (revision 14467)
+++ kernel/utility/cache.php (working copy)
@@ -90,6 +90,7 @@
$handler_class = $GLOBALS['vars']['CacheHandler'] . 'CacheHandler';
}
else {
+ $this->Application->Conn->nextQueryFromMaster = true;
$handler_class = $this->Application->ConfigValue('CacheHandler') . 'CacheHandler';
}
Index: kernel/utility/debugger.php
===================================================================
--- kernel/utility/debugger.php (revision 14467)
+++ kernel/utility/debugger.php (working copy)
@@ -896,8 +896,14 @@
$trace_count = count($trace_results);
$i = 0;
while ($i < $trace_count) {
+ if ( !isset($trace_results[$i]['file']) ) {
+ $i++;
+ continue;
+ }
+
$trace_file = basename($trace_results[$i]['file']);
- if ($trace_file != 'db_connection.php' && $trace_file != 'adodb.inc.php') {
+
+ if ($trace_file != 'db_connection.php' && $trace_file != 'db_load_balancer.php' && $trace_file != 'adodb.inc.php') {
break;
}
$i++;
@@ -954,7 +960,11 @@
$this->ProfilerData[$key]['subtitle'] = 'cachable';
}
- if (array_key_exists('prefix_special', $this->ProfilerData[$key])) {
+ if ($func_arguments[7]) {
+ $additional[] = Array ('name' => 'Server #', 'value' => $func_arguments[7]);
+ }
+
+ if ( isset($this->ProfilerData[$key]['prefix_special']) && $this->ProfilerData[$key]['prefix_special'] ) {
$additional[] = Array ('name' => 'PrefixSpecial', 'value' => $this->ProfilerData[$key]['prefix_special']);
}
db_load_balancer_v3_520.patch [^] (54,735 bytes) 2011-10-03 08:02
[Show Content]
Index: install.php
===================================================================
--- install.php (revision 14590)
+++ install.php (working copy)
@@ -226,12 +226,27 @@
$this->stepsPreset = $preset;
}
- function GetVar($name)
+ /**
+ * Returns variable from request
+ *
+ * @param string $name
+ * @return string|bool
+ * @access private
+ */
+ private function GetVar($name)
{
return array_key_exists($name, $_REQUEST) ? $_REQUEST[$name] : false;
}
- function SetVar($name, $value)
+ /**
+ * Sets new value for request variable
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ * @access private
+ */
+ private function SetVar($name, $value)
{
$_REQUEST[$name] = $value;
}
@@ -1177,6 +1192,8 @@
}
$upgrade_object = new $upgrade_classes[$module_path]();
+ /* @var $upgrade_object CoreUpgrades */
+
$upgrade_object->setToolkit($this->toolkit);
return $upgrade_object;
@@ -1254,17 +1271,12 @@
}
$this->Conn = new kDBConnection($this->toolkit->getSystemConfig('Database', 'DBType'), Array(&$this, 'DBErrorHandler'));
- $this->Conn->Connect(
- $this->toolkit->getSystemConfig('Database', 'DBHost'),
- $this->toolkit->getSystemConfig('Database', 'DBUser'),
- $this->toolkit->getSystemConfig('Database', 'DBUserPassword'),
- $this->toolkit->getSystemConfig('Database', 'DBName')
- );
+ $this->Conn->setup( $this->toolkit->systemConfig );
// setup toolkit too
$this->toolkit->Conn =& $this->Conn;
- return $this->Conn->errorCode == 0;
+ return !$this->Conn->hasError();
}
/**
Index: kernel/application.php
===================================================================
--- kernel/application.php (revision 14599)
+++ kernel/application.php (working copy)
@@ -302,9 +302,10 @@
$this->Factory = new kFactory();
$this->registerDefaultClasses();
- $this->Conn =& $this->Factory->makeClass('kDBConnection', Array (SQL_TYPE, Array (&$this, 'handleSQLError')));
- $this->Conn->debugMode = $this->isDebugMode();
- $this->Conn->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB);
+ $vars = kUtil::parseConfig(true);
+ $db_class = isset($vars['Databases']) ? 'kDBLoadBalancer' : 'kDBConnection';
+ $this->Conn =& $this->Factory->makeClass($db_class, Array (SQL_TYPE, Array (&$this, 'handleSQLError')));
+ $this->Conn->setup($vars);
$this->cacheManager =& $this->makeClass('kCacheManager');
$this->cacheManager->InitCache();
@@ -391,7 +392,7 @@
$language =& $this->recallObject('lang.current', null, Array ('live_table' => true));
/* @var $language LanguagesItem */
-
+
if ( preg_match('/utf-8/', $language->GetDBField('Charset')) ) {
setlocale(LC_ALL, 'en_US.UTF-8');
mb_internal_encoding('UTF-8');
@@ -729,6 +730,7 @@
// database
$this->registerClass('kDBConnection', KERNEL_PATH . '/db/db_connection.php');
+ $this->registerClass('kDBLoadBalancer', KERNEL_PATH . '/db/db_load_balancer.php');
$this->registerClass('kDBItem', KERNEL_PATH . '/db/dbitem.php');
$this->registerClass('kCatDBItem', KERNEL_PATH . '/db/cat_dbitem.php', null, 'kDBItem');
$this->registerClass('kDBList', KERNEL_PATH . '/db/dblist.php');
@@ -1207,7 +1209,7 @@
{
$session =& $this->recallObject('Session');
/* @var $session Session */
-
+
$session->Destroy();
}
@@ -1682,7 +1684,7 @@
{
return $this->cacheManager->SetConfigValue($name, $value);
}
-
+
/**
* Allows to process any type of event
*
Index: kernel/db/db_connection.php
===================================================================
--- kernel/db/db_connection.php (revision 14590)
+++ kernel/db/db_connection.php (working copy)
@@ -24,64 +24,81 @@
* Current database type
*
* @var string
- * @access private
+ * @access protected
*/
- var $dbType = 'mysql';
+ protected $dbType = 'mysql';
/**
* Created connection handle
*
* @var resource
- * @access private
+ * @access protected
*/
- var $connectionID = null;
+ protected $connectionID = null;
/**
+ * Remembers, that database connection was opened successfully
+ *
+ * @var bool
+ * @access public
+ */
+ public $connectionOpened = false;
+
+ /**
* Connection parameters, that were used
*
* @var Array
+ * @access protected
*/
- var $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');
+ protected $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => '');
/**
- * Handle of currenty processed recordset
+ * Index of database server
*
+ * @var int
+ * @access protected
+ */
+ protected $serverIndex = 0;
+
+ /**
+ * Handle of currently processed recordset
+ *
* @var resource
- * @access private
+ * @access protected
*/
- var $queryID = null;
+ protected $queryID = null;
/**
* DB type specific function mappings
*
* @var Array
- * @access private
+ * @access protected
*/
- var $metaFunctions = Array();
+ protected $metaFunctions = Array ();
/**
* Function to handle sql errors
*
- * @var string
- * @access private
+ * @var Array|string
+ * @access public
*/
- var $errorHandler = '';
+ public $errorHandler = '';
/**
* Error code
*
* @var int
- * @access private
+ * @access protected
*/
- var $errorCode = 0;
+ protected $errorCode = 0;
/**
* Error message
*
* @var string
- * @access private
+ * @access protected
*/
- var $errorMessage = '';
+ protected $errorMessage = '';
/**
* Defines if database connection
@@ -89,60 +106,77 @@
* information
*
* @var bool
+ * @access public
*/
- var $debugMode = false;
+ public $debugMode = false;
/**
* Save query execution statistics
*
* @var bool
+ * @access protected
*/
- var $_captureStatistics = false;
+ protected $_captureStatistics = false;
/**
* Last query to database
*
* @var string
+ * @access public
*/
- var $lastQuery = '';
+ public $lastQuery = '';
/**
* Total processed queries count
*
* @var int
+ * @access protected
*/
- var $_queryCount = 0;
+ protected $_queryCount = 0;
/**
* Total time, used for serving queries
*
* @var Array
+ * @access protected
*/
- var $_queryTime = 0;
+ protected $_queryTime = 0;
/**
* Indicates, that next database query could be cached, when memory caching is enabled
*
* @var bool
+ * @access public
*/
- var $nextQueryCachable = false;
+ public $nextQueryCachable = false;
/**
+ * For backwards compatibility with kDBLoadBalancer class
+ *
+ * @var bool
+ * @access public
+ */
+ public $nextQueryFromMaster = false;
+
+ /**
* Initializes connection class with
* db type to used in future
*
* @param string $dbType
- * @return DBConnection
+ * @param string $errorHandler
+ * @param int $server_index
* @access public
*/
- public function __construct($dbType, $errorHandler = '')
+ public function __construct($dbType, $errorHandler = '', $server_index = 0)
{
if ( class_exists('kApplication') ) {
// prevents "Fatal Error" on 2nd installation step (when database is empty)
parent::__construct();
- }
+ }
$this->dbType = $dbType;
+ $this->serverIndex = $server_index;
+
// $this->initMetaFunctions();
if (!$errorHandler) {
$this->errorHandler = Array(&$this, 'handleError');
@@ -159,9 +193,9 @@
*
* @param int $code
* @param string $msg
- * @access public
+ * @access protected
*/
- function setError($code, $msg)
+ protected function setError($code, $msg)
{
$this->errorCode = $code;
$this->errorMessage = $msg;
@@ -174,7 +208,7 @@
* @return bool
* @access public
*/
- function hasError()
+ public function hasError()
{
return $this->errorCode != 0;
}
@@ -183,38 +217,37 @@
* Caches function specific to requested
* db type
*
- * @access private
+ * @access protected
*/
- function initMetaFunctions()
+ protected function initMetaFunctions()
{
- $ret = Array();
- switch ($this->dbType)
- {
+ $ret = Array ();
+
+ switch ( $this->dbType ) {
case 'mysql':
- $ret = Array(); // only define functions, that name differs from "dbType_<meta_name>"
+ $ret = Array (); // only define functions, that name differs from "dbType_<meta_name>"
break;
+ }
-
- }
$this->metaFunctions = $ret;
}
/**
- * Get's function for specific db type
+ * Gets function for specific db type
* based on it's meta name
*
* @param string $name
* @return string
- * @access private
+ * @access protected
*/
- function getMetaFunction($name)
+ protected function getMetaFunction($name)
{
- /*if (!isset($this->metaFunctions[$name])) {
+ /*if ( !isset($this->metaFunctions[$name]) ) {
$this->metaFunctions[$name] = $name;
}*/
- return $this->dbType.'_'.$name;
+ return $this->dbType . '_' . $name;
}
@@ -227,9 +260,12 @@
* @param string $user
* @param string $pass
* @param string $db
+ * @param bool $force_new
+ * @param bool $retry
+ * @return bool
* @access public
*/
- function Connect($host, $user, $pass, $db, $force_new = false, $retry = false)
+ public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false)
{
$this->connectionParams = Array ('host' => $host, 'user' => $user, 'pass' => $pass, 'db' => $db);
@@ -253,7 +289,9 @@
$func = $this->getMetaFunction('errno');
$this->errorCode = $this->connectionID ? $func($this->connectionID) : $func();
- if ( !$this->hasError() ) {
+ if ( is_resource($this->connectionID) && !$this->hasError() ) {
+ $this->connectionOpened = true;
+
return true;
}
@@ -268,17 +306,48 @@
throw new Exception($error_msg);
}
+ $this->connectionOpened = false;
+
return false;
}
- function ReConnect($force_new = false)
+ /**
+ * Setups the connection according given configuration
+ *
+ * @param Array $config
+ * @return bool
+ * @access public
+ */
+ public function setup($config)
{
+ if ( is_object($this->Application) ) {
+ $this->debugMode = $this->Application->isDebugMode();
+ }
+
+ return $this->Connect(
+ $config['Database']['DBHost'],
+ $config['Database']['DBUser'],
+ $config['Database']['DBUserPassword'],
+ $config['Database']['DBName']
+ );
+ }
+
+ /**
+ * Performs 3 reconnect attempts in case if connection to a DB was lost in the middle of script run (e.g. server restart)
+ *
+ * @param bool $force_new
+ * @return bool
+ * @access protected
+ */
+ protected function ReConnect($force_new = false)
+ {
$retry_count = 0;
+ $connected = false;
$func = $this->getMetaFunction('close');
$func($this->connectionID);
- while ($retry_count < 3) {
+ while ( $retry_count < 3 ) {
sleep(5); // wait 5 seconds before each reconnect attempt
$connected = $this->Connect(
@@ -289,7 +358,7 @@
$force_new, true
);
- if ($connected) {
+ if ( $connected ) {
break;
}
@@ -303,12 +372,16 @@
* Shows error message from previous operation
* if it failed
*
- * @access private
+ * @param string $sql
+ * @param string $key_field
+ * @param bool $no_debug
+ * @return bool
+ * @access protected
*/
- function showError($sql = '', $key_field = null, $no_debug = false)
+ protected function showError($sql = '', $key_field = null, $no_debug = false)
{
static $retry_count = 0;
-
+
$func = $this->getMetaFunction('errno');
if (!$this->connectionID) {
@@ -359,7 +432,14 @@
return false;
}
- function callErrorHandler($sql)
+ /**
+ * Sends db error to a predefined error handler
+ *
+ * @param $sql
+ * @return bool
+ * @access protected
+ */
+ protected function callErrorHandler($sql)
{
if (is_array($this->errorHandler)) {
$func = $this->errorHandler[1];
@@ -380,9 +460,9 @@
* @param string $msg
* @param string $sql
* @return bool
- * @access private
+ * @access public
*/
- function handleError($code, $msg, $sql)
+ public function handleError($code, $msg, $sql)
{
echo '<strong>Processing SQL</strong>: ' . $sql . '<br/>';
echo '<strong>Error (' . $code . '):</strong> ' . $msg . '<br/>';
@@ -396,9 +476,9 @@
*
* @param string $new_name
* @return bool
- * @access public
+ * @access protected
*/
- function setDB($new_name)
+ protected function setDB($new_name)
{
if (!$this->connectionID) return false;
$func = $this->getMetaFunction('select_db');
@@ -415,11 +495,14 @@
* @return string
* @access public
*/
- function GetOne($sql, $offset = 0)
+ public function GetOne($sql, $offset = 0)
{
$row = $this->GetRow($sql, $offset);
- if(!$row) return false;
+ if ( !$row ) {
+ return false;
+ }
+
return array_shift($row);
}
@@ -432,12 +515,15 @@
* @return Array
* @access public
*/
- function GetRow($sql, $offset = 0)
+ public function GetRow($sql, $offset = 0)
{
- $sql .= ' '.$this->getLimitClause($offset, 1);
+ $sql .= ' ' . $this->getLimitClause($offset, 1);
$ret = $this->Query($sql);
- if(!$ret) return false;
+ if ( !$ret ) {
+ return false;
+ }
+
return array_shift($ret);
}
@@ -453,25 +539,30 @@
* @return Array
* @access public
*/
- function GetCol($sql, $key_field = null)
+ public function GetCol($sql, $key_field = null)
{
$rows = $this->Query($sql);
- if (!$rows) return $rows;
+ if ( !$rows ) {
+ return $rows;
+ }
- $i = 0; $row_count = count($rows);
- $ret = Array();
- if (isset($key_field)) {
- while ($i < $row_count) {
+ $i = 0;
+ $row_count = count($rows);
+ $ret = Array ();
+
+ if ( isset($key_field) ) {
+ while ( $i < $row_count ) {
$ret[$rows[$i][$key_field]] = array_shift($rows[$i]);
$i++;
}
}
else {
- while ($i < $row_count) {
+ while ( $i < $row_count ) {
$ret[] = array_shift($rows[$i]);
$i++;
}
}
+
return $ret;
}
@@ -484,46 +575,48 @@
*
* @param string $sql
* @param string $key_field
+ * @param bool $no_debug
* @return Array
+ * @access public
*/
- function Query($sql, $key_field = null, $no_debug = false)
+ public function Query($sql, $key_field = null, $no_debug = false)
{
$this->lastQuery = $sql;
- if (!$no_debug) {
+ if ( !$no_debug ) {
$this->_queryCount++;
}
- if ($this->debugMode && !$no_debug) {
- return $this->debugQuery($sql,$key_field);
+ if ( $this->debugMode && !$no_debug ) {
+ return $this->debugQuery($sql, $key_field);
}
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
- if ($this->_captureStatistics) {
+ if ( $this->_captureStatistics ) {
$start_time = microtime(true);
}
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
- $this->queryID = $query_func($sql,$this->connectionID);
- if (is_resource($this->queryID)) {
- $ret = Array();
+ $this->queryID = $query_func($sql, $this->connectionID);
+ if ( is_resource($this->queryID) ) {
+ $ret = Array ();
$fetch_func = $this->getMetaFunction('fetch_assoc');
- if (isset($key_field)) {
- while (($row = $fetch_func($this->queryID))) {
+ if ( isset($key_field) ) {
+ while ( ($row = $fetch_func($this->queryID)) ) {
$ret[$row[$key_field]] = $row;
}
}
else {
- while (($row = $fetch_func($this->queryID))) {
+ while ( ($row = $fetch_func($this->queryID)) ) {
$ret[] = $row;
}
}
// set 2nd checkpoint: begin
- if ($this->_captureStatistics) {
+ if ( $this->_captureStatistics ) {
$query_time = microtime(true) - $start_time;
- if ($query_time > DBG_MAX_SQL_TIME && !$no_debug) {
+ if ( $query_time > DBG_MAX_SQL_TIME && !$no_debug ) {
$this->Application->logSlowQuery($sql, $query_time);
}
$this->_queryTime += $query_time;
@@ -535,7 +628,7 @@
}
else {
// set 2nd checkpoint: begin
- if ($this->_captureStatistics) {
+ if ( $this->_captureStatistics ) {
$this->_queryTime += microtime(true) - $start_time;
}
// set 2nd checkpoint: end
@@ -544,57 +637,73 @@
return $this->showError($sql, $key_field, $no_debug);
}
- function ChangeQuery($sql)
+ /**
+ * Performs sql query, that will change database content
+ *
+ * @param string $sql
+ * @return bool
+ * @access public
+ */
+ public function ChangeQuery($sql)
{
$this->Query($sql);
return $this->errorCode == 0 ? true : false;
}
- function debugQuery($sql, $key_field = null)
+ /**
+ * Queries db with $sql query supplied
+ * and returns rows selected if any, false
+ * otherwise. Optional parameter $key_field
+ * allows to set one of the query fields
+ * value as key in string array.
+ *
+ * Each database query will be profiled into Debugger.
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ * @access public
+ */
+ protected function debugQuery($sql, $key_field = null)
{
global $debugger;
$query_func = $this->getMetaFunction('query');
// set 1st checkpoint: begin
$profileSQLs = defined('DBG_SQL_PROFILE') && DBG_SQL_PROFILE;
- if ($profileSQLs) {
+ if ( $profileSQLs ) {
$queryID = $debugger->generateID();
- $debugger->profileStart('sql_'.$queryID, $debugger->formatSQL($sql));
+ $debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql));
}
// set 1st checkpoint: end
$this->setError(0, ''); // reset error
$this->queryID = $query_func($sql, $this->connectionID);
- if( is_resource($this->queryID) )
- {
- $ret = Array();
+ if ( is_resource($this->queryID) ) {
+ $ret = Array ();
$fetch_func = $this->getMetaFunction('fetch_assoc');
- if( isset($key_field) )
- {
- while( ($row = $fetch_func($this->queryID)) )
- {
+ if ( isset($key_field) ) {
+ while ( ($row = $fetch_func($this->queryID)) ) {
$ret[$row[$key_field]] = $row;
}
}
- else
- {
- while( ($row = $fetch_func($this->queryID)) )
- {
+ else {
+ while ( ($row = $fetch_func($this->queryID)) ) {
$ret[] = $row;
}
}
// set 2nd checkpoint: begin
- if ($profileSQLs) {
+ if ( $profileSQLs ) {
$first_cell = count($ret) == 1 && count(current($ret)) == 1 ? current(current($ret)) : null;
- if (strlen($first_cell) > 200) {
- $first_cell = substr($first_cell, 0, 50) . ' ...';
+ if ( strlen($first_cell) > 200 ) {
+ $first_cell = substr($first_cell, 0, 50) . ' ...';
}
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable);
- $debugger->profilerAddTotal('sql', 'sql_'.$queryID);
+ $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
+ $debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
@@ -604,9 +713,9 @@
}
else {
// set 2nd checkpoint: begin
- if ($profileSQLs) {
- $debugger->profileFinish('sql_'.$queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable);
- $debugger->profilerAddTotal('sql', 'sql_'.$queryID);
+ if ( $profileSQLs ) {
+ $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex);
+ $debugger->profilerAddTotal('sql', 'sql_' . $queryID);
$this->nextQueryCachable = false;
}
// set 2nd checkpoint: end
@@ -618,12 +727,11 @@
/**
* Free memory used to hold recordset handle
*
- * @access private
+ * @access public
*/
- function Destroy()
+ public function Destroy()
{
- if($this->queryID)
- {
+ if ( $this->queryID ) {
$free_func = $this->getMetaFunction('free_result');
$free_func($this->queryID);
$this->queryID = null;
@@ -637,7 +745,7 @@
* @return int
* @access public
*/
- function getInsertID()
+ public function getInsertID()
{
$func = $this->getMetaFunction('insert_id');
return $func($this->connectionID);
@@ -649,7 +757,7 @@
* @return int
* @access public
*/
- function getAffectedRows()
+ public function getAffectedRows()
{
$func = $this->getMetaFunction('affected_rows');
return $func($this->connectionID);
@@ -661,16 +769,17 @@
* @param int $offset
* @param int $rows
* @return string
- * @access private
+ * @access public
*/
- function getLimitClause($offset, $rows)
+ public function getLimitClause($offset, $rows)
{
- if(!($rows > 0)) return '';
+ if ( !($rows > 0) ) {
+ return '';
+ }
- switch ($this->dbType) {
-
+ switch ( $this->dbType ) {
default:
- return 'LIMIT '.$offset.','.$rows;
+ return 'LIMIT ' . $offset . ',' . $rows;
break;
}
}
@@ -680,10 +789,10 @@
* Otherwise returns as-is
*
* @param mixed $string
- *
* @return string
+ * @access public
*/
- function qstr($string)
+ public function qstr($string)
{
if ( is_null($string) ) {
return 'NULL';
@@ -700,10 +809,10 @@
* Escapes strings (only work since PHP 4.3.0)
*
* @param mixed $string
- *
* @return string
+ * @access public
*/
- function escape($string)
+ public function escape($string)
{
if ( is_null($string) ) {
return 'NULL';
@@ -716,11 +825,12 @@
}
/**
- * Returns last error code occured
+ * Returns last error code occurred
*
* @return int
+ * @access public
*/
- function getErrorCode()
+ public function getErrorCode()
{
return $this->errorCode;
}
@@ -731,7 +841,7 @@
* @return string
* @access public
*/
- function getErrorMsg()
+ public function getErrorMsg()
{
return $this->errorMessage;
}
@@ -745,8 +855,9 @@
* @param string $type
* @param bool $insert_now
* @return bool
+ * @access public
*/
- function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
+ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
{
static $value_sqls = Array ();
@@ -779,7 +890,16 @@
return $insert_result;
}
- function doUpdate($fields_hash, $table, $key_clause)
+ /**
+ * Update given field values to given record using $key_clause
+ *
+ * @param Array $fields_hash
+ * @param string $table
+ * @param string $key_clause
+ * @return bool
+ * @access public
+ */
+ public function doUpdate($fields_hash, $table, $key_clause)
{
if (!$fields_hash) return true;
@@ -797,12 +917,13 @@
}
/**
- * Allows to detect table's presense in database
+ * Allows to detect table's presence in database
*
* @param string $table_name
* @return bool
+ * @access public
*/
- function TableFound($table_name)
+ public function TableFound($table_name)
{
static $table_found = Array();
@@ -821,9 +942,59 @@
* Returns query processing statistics
*
* @return Array
+ * @access public
*/
- function getQueryStatistics()
+ public function getQueryStatistics()
{
return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount);
}
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ *
+ * @param string $which
+ * @return Array
+ * @access public
+ */
+ public function getStatus($which = '%')
+ {
+ $status = Array ();
+ $records = $this->Query('SHOW STATUS LIKE "' . $which . '"');
+
+ foreach ($records as $record) {
+ $status[ $record['Variable_name'] ] = $record['Value'];
+ }
+
+ return $status;
+ }
+
+ /**
+ * Get slave replication lag. It will only work if the DB user has the PROCESS privilege.
+ *
+ * @return int
+ * @access public
+ */
+ public function getSlaveLag()
+ {
+ // don't use kDBConnection::Query method, since it will create an array of all server processes
+ $rs = mysql_query('SHOW PROCESSLIST', $this->connectionID);
+
+ $skip_states = Array (
+ 'Waiting for master to send event',
+ 'Connecting to master',
+ 'Queueing master event to the relay log',
+ 'Waiting for master update',
+ 'Requesting binlog dump',
+ );
+
+ // find slave SQL thread
+ while ( $row = mysql_fetch_array($rs) ) {
+ if ( $row['User'] == 'system user' && !in_array($row['State'], $skip_states) ) {
+ // this is it, return the time (except -ve)
+ return $row['Time'] > 0x7fffffff ? false : $row['Time'];
+ }
+ }
+
+ return false;
+ }
}
\ No newline at end of file
Index: kernel/db/db_load_balancer.php
===================================================================
--- kernel/db/db_load_balancer.php (revision 0)
+++ kernel/db/db_load_balancer.php (revision 0)
@@ -0,0 +1,627 @@
+<?php
+
+class kDBLoadBalancer extends kBase {
+
+ /**
+ * Current database type
+ *
+ * @var string
+ * @access protected
+ */
+ protected $dbType = 'mysql';
+
+ /**
+ * Function to handle sql errors
+ *
+ * @var string
+ * @access public
+ */
+ public $errorHandler = '';
+
+ /**
+ * Database connection settings
+ *
+ * @var Array
+ * @access protected
+ */
+ protected $servers = Array ();
+
+ /**
+ * Load of each slave server, given
+ *
+ * @var Array
+ * @access protected
+ */
+ protected $serverLoads = Array ();
+
+ /**
+ * Caches replication lag of servers
+ *
+ * @var Array
+ * @access protected
+ */
+ protected $serverLagTimes = Array ();
+
+ /**
+ * Connection references to opened connections
+ *
+ * @var Array
+ * @access protected
+ */
+ protected $connections = Array ();
+
+ /**
+ * Index of last user slave connection
+ *
+ * @var int
+ * @access protected
+ */
+ protected $slaveIndex = false;
+
+ /**
+ * Index of the server, that was used last
+ *
+ * @var int
+ * @access protected
+ */
+ protected $lastUsedIndex = 0;
+
+ /**
+ * Consider slave down if it isn't responding for N milliseconds
+ *
+ * @var int
+ * @access protected
+ */
+ protected $DBClusterTimeout = 10;
+
+ /**
+ * Scale load balancer polling time so that under overload conditions, the database server
+ * receives a SHOW STATUS query at an average interval of this many microseconds
+ *
+ * @var int
+ * @access protected
+ */
+ protected $DBAvgStatusPoll = 2000;
+
+ /**
+ * Indicates, that next database query could be cached, when memory caching is enabled
+ *
+ * @var bool
+ * @access public
+ */
+ public $nextQueryCachable = false;
+
+ /**
+ * Indicates, that next query should be sent to maser database
+ *
+ * @var bool
+ */
+ public $nextQueryFromMaster = false;
+
+ /**
+ * Creates new instance of load balancer class
+ *
+ * @param string $dbType
+ * @param Array|string $errorHandler
+ */
+ function __construct($dbType, $errorHandler = '')
+ {
+ parent::__construct();
+
+ $this->dbType = $dbType;
+ $this->errorHandler = $errorHandler;
+
+ $this->DBClusterTimeout *= 1e6; // convert to milliseconds
+ }
+
+ /**
+ * Setups load balancer according given configuration
+ *
+ * @param Array $config
+ * @return void
+ * @access public
+ */
+ public function setup($config)
+ {
+ $this->servers = Array ();
+
+ $this->servers[0] = Array (
+ 'DBHost' => $config['Database']['DBHost'],
+ 'DBUser' => $config['Database']['DBUser'],
+ 'DBUserPassword' => $config['Database']['DBUserPassword'],
+ 'DBName' => $config['Database']['DBName'],
+ 'DBLoad' => 0,
+ );
+
+ if ( isset($config['Databases']) ) {
+ $this->servers = array_merge($this->servers, $config['Databases']);
+ }
+
+ foreach ($this->servers as $server_index => $server_setting) {
+ $this->serverLoads[$server_index] = $server_setting['DBLoad'];
+ }
+ }
+
+ /**
+ * Returns connection index to master database
+ *
+ * @return int
+ * @access protected
+ */
+ protected function getMasterIndex()
+ {
+ return 0;
+ }
+
+ /**
+ * Returns connection index to slave database. This takes into account load ratios and lag times.
+ * Side effect: opens connections to databases
+ *
+ * @return int
+ * @access protected
+ */
+ protected function getSlaveIndex()
+ {
+ if ( count($this->servers) == 1 || $this->Application->isAdmin ) {
+ // skip the load balancing if there's only one server OR in admin console
+ return 0;
+ }
+ elseif ( $this->slaveIndex !== false ) {
+ // shortcut if generic reader exists already
+ return $this->slaveIndex;
+ }
+
+ $total_elapsed = 0;
+ $non_error_loads = $this->serverLoads;
+ $i = $found = $lagged_slave_mode = false;
+
+ // first try quickly looking through the available servers for a server that meets our criteria
+ do {
+ $current_loads = $non_error_loads;
+ $overloaded_servers = $total_threads_connected = 0;
+
+ while ($current_loads) {
+ if ( $lagged_slave_mode ) {
+ // when all slave servers are too lagged, then ignore lag and pick random server
+ $i = $this->pickRandom($current_loads);
+ }
+ else {
+ $i = $this->getRandomNonLagged($current_loads);
+
+ if ( $i === false && $current_loads ) {
+ // all slaves lagged -> pick random lagged slave then
+ $lagged_slave_mode = true;
+ $i = $this->pickRandom( $current_loads );
+ }
+ }
+
+ if ( $i === false ) {
+ // all slaves are down -> use master as a slave
+ $this->slaveIndex = $this->getMasterIndex();
+
+ return $this->slaveIndex;
+ }
+
+ $conn =& $this->openConnection($i);
+
+ if ( !$conn ) {
+ unset($non_error_loads[$i], $current_loads[$i]);
+ continue;
+ }
+
+ // Perform post-connection backoff
+ $threshold = isset($this->servers[$i]['DBMaxThreads']) ? $this->servers[$i]['DBMaxThreads'] : false;
+ $backoff = $this->postConnectionBackoff($conn, $threshold);
+
+ if ( $backoff ) {
+ // post-connection overload, don't use this server for now
+ $total_threads_connected += $backoff;
+ $overloaded_servers++;
+
+ unset( $current_loads[$i] );
+ }
+ else {
+ // return this server
+ break 2;
+ }
+ }
+
+ // no server found yet
+ $i = false;
+
+ // if all servers were down, quit now
+ if ( !$non_error_loads ) {
+ break;
+ }
+
+ // back off for a while
+ // scale the sleep time by the number of connected threads, to produce a roughly constant global poll rate
+ $avg_threads = $total_threads_connected / $overloaded_servers;
+
+ usleep($this->DBAvgStatusPoll * $avg_threads);
+ $total_elapsed += $this->DBAvgStatusPoll * $avg_threads;
+ } while ( $total_elapsed < $this->DBClusterTimeout );
+
+ if ( $i !== false ) {
+ // slave connection successful
+ if ( $this->slaveIndex <= 0 && $this->serverLoads[$i] > 0 && $i !== false ) {
+ $this->slaveIndex = $i;
+ }
+ }
+
+ return $i;
+ }
+
+ /**
+ * Returns random non-lagged server
+ *
+ * @param Array $loads
+ * @return int
+ * @access protected
+ */
+ protected function getRandomNonLagged($loads)
+ {
+ // unset excessively lagged servers
+ $lags = $this->getLagTimes();
+
+ foreach ($lags as $i => $lag) {
+ if ( $i != 0 && isset($this->servers[$i]['DBMaxLag']) ) {
+ if ( $lag === false ) {
+ unset( $loads[$i] ); // server is not replicating
+ }
+ elseif ( $lag > $this->servers[$i]['DBMaxLag'] ) {
+ unset( $loads[$i] ); // server is excessively lagged
+ }
+ }
+ }
+
+ // find out if all the slaves with non-zero load are lagged
+ if ( !$loads || array_sum($loads) == 0 ) {
+ return false;
+ }
+
+ // return a random representative of the remainder
+ return $this->pickRandom($loads);
+ }
+
+ /**
+ * Select an element from an array of non-normalised probabilities
+ *
+ * @param Array $weights
+ * @return int
+ * @access protected
+ */
+ protected function pickRandom($weights)
+ {
+ if ( !is_array($weights) || !$weights ) {
+ return false;
+ }
+
+ $sum = array_sum($weights);
+
+ if ( $sum == 0 ) {
+ return false;
+ }
+
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $index = $sum = 0;
+
+ foreach ($weights as $index => $weight) {
+ $sum += $weight;
+
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+
+ return $index;
+ }
+
+ /**
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
+ *
+ * @return Array
+ * @access protected
+ */
+ protected function getLagTimes()
+ {
+ if ( $this->serverLagTimes ) {
+ return $this->serverLagTimes;
+ }
+
+ $expiry = 5;
+ $request_rate = 10;
+
+ $cache_key = 'lag_times:' . $this->servers[0]['DBHost'];
+ $times = $this->Application->getCache($cache_key);
+
+ if ( $times ) {
+ // randomly recache with probability rising over $expiry
+ $elapsed = adodb_mktime() - $times['timestamp'];
+ $chance = max(0, ($expiry - $elapsed) * $request_rate);
+
+ if ( mt_rand(0, $chance) != 0 ) {
+ unset( $times['timestamp'] );
+ $this->serverLagTimes = $times;
+
+ return $times;
+ }
+ }
+
+ // cache key missing or expired
+ $times = Array();
+
+ foreach ($this->servers as $index => $server) {
+ if ($index == 0) {
+ $times[$index] = 0; // master
+ }
+ else {
+ $conn =& $this->openConnection($index);
+
+ if ($conn !== false) {
+ $times[$index] = $conn->getSlaveLag();
+ }
+ }
+ }
+
+ // add a timestamp key so we know when it was cached
+ $times['timestamp'] = adodb_mktime();
+ $this->Application->setCache($cache_key, $times, $expiry);
+
+ // but don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $this->serverLagTimes = $times;
+
+ return $this->serverLagTimes;
+ }
+
+ /**
+ * Determines whatever server should not be used, even, when connection was made
+ *
+ * @param kDBConnection $conn
+ * @param int $threshold
+ * @return int
+ * @access protected
+ */
+ protected function postConnectionBackoff(&$conn, $threshold)
+ {
+ if ( !$threshold ) {
+ return 0;
+ }
+
+ $status = $conn->getStatus('Thread%');
+
+ return $status['Threads_running'] > $threshold ? $status['Threads_connected'] : 0;
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false.
+ *
+ * @param integer $i Server index
+ * @return kDBConnection|false
+ * @access protected
+ */
+ protected function &openConnection($i)
+ {
+ if ( isset($this->connections[$i]) ) {
+ $conn =& $this->connections[$i];
+ }
+ else {
+ $server = $this->servers[$i];
+ $server['serverIndex'] = $i;
+ $conn =& $this->reallyOpenConnection($server);
+
+ if ( $conn->connectionOpened ) {
+ $this->connections[$i] =& $conn;
+ $this->lastUsedIndex = $i;
+ }
+ else {
+ $conn = false;
+ }
+ }
+
+ if ( $this->nextQueryCachable && is_object($conn) ) {
+ $conn->nextQueryCachable = true;
+ $this->nextQueryCachable = false;
+ }
+
+ return $conn;
+ }
+
+ /**
+ * Really opens a connection.
+ * Returns a database object whether or not the connection was successful.
+ *
+ * @param Array $server
+ * @return kDBConnection
+ */
+ protected function &reallyOpenConnection($server)
+ {
+ $db =& $this->Application->makeClass( 'kDBConnection', Array ($this->dbType, $this->errorHandler, $server['serverIndex']) );
+ /* @var $db kDBConnection */
+
+ $db->debugMode = $this->Application->isDebugMode();
+ $db->Connect($server['DBHost'], $server['DBUser'], $server['DBUserPassword'], $this->servers[0]['DBName'], true, true);
+
+ return $db;
+ }
+
+ /**
+ * Returns first field of first line of recordset if query ok or false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return string
+ * @access public
+ */
+ public function GetOne($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetOne($sql, $offset);
+ }
+
+ /**
+ * Returns first row of recordset if query ok, false otherwise
+ *
+ * @param string $sql
+ * @param int $offset
+ * @return Array
+ * @access public
+ */
+ public function GetRow($sql, $offset = 0)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetRow($sql, $offset);
+ }
+
+ /**
+ * Returns 1st column of recordset as one-dimensional array or false otherwise
+ * Optional parameter $key_field can be used to set field name to be used as resulting array key
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @return Array
+ * @access public
+ */
+ public function GetCol($sql, $key_field = null)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->GetCol($sql, $key_field);
+ }
+
+ /**
+ * Queries db with $sql query supplied and returns rows selected if any, false
+ * otherwise. Optional parameter $key_field allows to set one of the query fields
+ * value as key in string array.
+ *
+ * @param string $sql
+ * @param string $key_field
+ * @param bool $no_debug
+ * @return Array
+ * @access public
+ */
+ public function Query($sql, $key_field = null, $no_debug = false)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->Query($sql, $key_field, $no_debug);
+ }
+
+ /**
+ * Performs sql query, that will change database content
+ *
+ * @param string $sql
+ * @return bool
+ * @access public
+ */
+ public function ChangeQuery($sql)
+ {
+ $conn =& $this->chooseConnection($sql);
+
+ return $conn->ChangeQuery($sql);
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0)
+ * Otherwise returns as-is
+ *
+ * @param mixed $string
+ * @return string
+ * @access public
+ */
+ public function qstr($string)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return $conn->qstr($string);
+ }
+
+ /**
+ * Performs insert of given data (useful with small number of queries)
+ * or stores it to perform multiple insert later (useful with large number of queries)
+ *
+ * @param Array $fields_hash
+ * @param string $table
+ * @param string $type
+ * @param bool $insert_now
+ * @return bool
+ * @access public
+ */
+ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true)
+ {
+ $conn =& $this->openConnection( $this->getMasterIndex() );
+
+ return $conn->doInsert($fields_hash, $table, $type, $insert_now);
+ }
+
+ /**
+ * Update given field values to given record using $key_clause
+ *
+ * @param Array $fields_hash
+ * @param string $table
+ * @param string $key_clause
+ * @return bool
+ * @access public
+ */
+ public function doUpdate($fields_hash, $table, $key_clause)
+ {
+ $conn =& $this->openConnection( $this->getMasterIndex() );
+
+ return $conn->doUpdate($fields_hash, $table, $key_clause);
+ }
+
+ /**
+ * When undefined method is called, then send it directly to last used slave server connection
+ *
+ * @param string $name
+ * @param Array $arguments
+ * @return mixed
+ * @access public
+ */
+ public function __call($name, $arguments)
+ {
+ $conn =& $this->openConnection($this->lastUsedIndex);
+
+ return call_user_func_array( Array (&$conn, $name), $arguments );
+ }
+
+ /**
+ * Returns appropriate connection based on sql
+ *
+ * @param string $sql
+ * @return kDBConnection
+ * @access protected
+ */
+ protected function &chooseConnection($sql)
+ {
+ if ( $this->nextQueryFromMaster ) {
+ $this->nextQueryFromMaster = false;
+ $index = $this->getMasterIndex();
+ }
+ else {
+ $sid = isset($this->Application->Session) ? $this->Application->GetSID() : '9999999999999999999999';
+
+ if ( preg_match('/(^[ \t\r\n]*(ALTER|CREATE|DROP|RENAME|DELETE|DO|INSERT|LOAD|REPLACE|TRUNCATE|UPDATE))|ses_' . $sid . '/', $sql) ) {
+ $index = $this->getMasterIndex();
+ }
+ else {
+ $index = $this->getSlaveIndex();
+ }
+ }
+
+ $this->lastUsedIndex = $index;
+ $conn =& $this->openConnection($index);
+
+ return $conn;
+ }
+}
Property changes on: kernel\db\db_load_balancer.php
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ LF
Index: kernel/globals.php
===================================================================
--- kernel/globals.php (revision 14590)
+++ kernel/globals.php (working copy)
@@ -88,20 +88,21 @@
public static function print_r($data, $label = '', $on_screen = false)
{
$is_debug = false;
- if (class_exists('kApplication') && !$on_screen) {
+
+ if ( class_exists('kApplication') && !$on_screen ) {
$application =& kApplication::Instance();
$is_debug = $application->isDebugMode();
}
- if ($is_debug) {
- if ($label) {
+ if ( $is_debug && isset($application) ) {
+ if ( $label ) {
$application->Debugger->appendHTML('<strong>' . $label . '</strong>');
}
$application->Debugger->dumpVars($data);
}
else {
- if ($label) {
+ if ( $label ) {
echo '<strong>' . $label . '</strong><br/>';
}
@@ -155,6 +156,10 @@
require($file);
if ($parse_section) {
+ if ( isset($_CONFIG['Database']['LoadBalancing']) && $_CONFIG['Database']['LoadBalancing'] ) {
+ require FULL_PATH . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR . 'db_servers.php';
+ }
+
return $_CONFIG;
}
@@ -316,6 +321,9 @@
*
* @param string $url
* @param mixed $data
+ * @param Array $headers
+ * @param string $request_type
+ * @param Array $curl_options
* @return string
* @access public
* @deprecated
Index: kernel/startup.php
===================================================================
--- kernel/startup.php (revision 14590)
+++ kernel/startup.php (working copy)
@@ -177,6 +177,7 @@
FULL_PATH . APPLICATION_PATH,
KERNEL_PATH . "/kbase.php",
KERNEL_PATH . '/db/db_connection.php',
+ KERNEL_PATH . '/db/db_load_balancer.php',
KERNEL_PATH . '/utility/event.php',
KERNEL_PATH . "/utility/factory.php",
KERNEL_PATH . "/languages/phrases_cache.php",
Index: kernel/utility/cache.php
===================================================================
--- kernel/utility/cache.php (revision 14590)
+++ kernel/utility/cache.php (working copy)
@@ -114,6 +114,7 @@
$handler_class = $vars['CacheHandler'] . 'CacheHandler';
}
else {
+ $this->Application->Conn->nextQueryFromMaster = true;
$handler_class = $this->Application->ConfigValue('CacheHandler') . 'CacheHandler';
}
Index: kernel/utility/debugger.php
===================================================================
--- kernel/utility/debugger.php (revision 14590)
+++ kernel/utility/debugger.php (working copy)
@@ -109,7 +109,7 @@
if ( preg_replace('/[\d\.\/-]/', '', $network) != '' ) {
$network = gethostbyname($network);
}
-
+
if ($network == $ip) {
// comparing two ip addresses directly
return true;
@@ -156,7 +156,7 @@
var $Application = null;
/**
- * Set to true if fatal error occured
+ * Set to true if fatal error occurred
*
* @var bool
*/
@@ -358,18 +358,20 @@
function InitReport()
{
- if (!class_exists('kApplication')) return false;
+ if ( !class_exists('kApplication') ) {
+ return ;
+ }
$application =& kApplication::Instance();
// string used to separate debugger records while in file (used in debugger dump filename too)
- $this->rowSeparator = '@'.(is_object($application->Factory) ? $application->GetSID() : 0).'@';
+ $this->rowSeparator = '@' . (is_object($application->Factory) ? $application->GetSID() : 0) . '@';
// $this->rowSeparator = '@'.rand(0,100000).'@';
// include debugger files from this url
- $reg_exp = '/^'.preg_quote(FULL_PATH, '/').'/';
+ $reg_exp = '/^' . preg_quote(FULL_PATH, '/') . '/';
$kernel_path = preg_replace($reg_exp, '', KERNEL_PATH, 1);
- $this->baseURL = PROTOCOL.SERVER_NAME.(defined('PORT') ? ':'.PORT : '').rtrim(BASE_PATH, '/').$kernel_path.'/utility/debugger';
+ $this->baseURL = PROTOCOL . SERVER_NAME . (defined('PORT') ? ':' . PORT : '') . rtrim(BASE_PATH, '/') . $kernel_path . '/utility/debugger';
// save debug output in this folder
$this->tempFolder = defined('RESTRICTED') ? RESTRICTED : WRITEABLE . '/cache';
@@ -965,8 +967,13 @@
$trace_count = count($trace_results);
$i = 0;
while ($i < $trace_count) {
+ if ( !isset($trace_results[$i]['file']) ) {
+ $i++;
+ continue;
+ }
+
$trace_file = basename($trace_results[$i]['file']);
- if ($trace_file != 'db_connection.php' && $trace_file != 'adodb.inc.php') {
+ if ($trace_file != 'db_connection.php' && $trace_file != 'db_load_balancer.php' && $trace_file != 'adodb.inc.php') {
break;
}
$i++;
@@ -977,6 +984,8 @@
if (array_key_exists('object', $trace_results[$i + 1]) && isset($trace_results[$i + 1]['object']->Prefix)) {
$object =& $trace_results[$i + 1]['object'];
+ /* @var $object kBase */
+
$prefix_special = rtrim($object->Prefix . '.' . $object->Special, '.');
$this->ProfilerData[$key]['prefix_special'] = $prefix_special;
}
@@ -1023,6 +1032,10 @@
$this->ProfilerData[$key]['subtitle'] = 'cachable';
}
+ if ($func_arguments[7]) {
+ $additional[] = Array ('name' => 'Server #', 'value' => $func_arguments[7]);
+ }
+
if (array_key_exists('prefix_special', $this->ProfilerData[$key])) {
$additional[] = Array ('name' => 'PrefixSpecial', 'value' => $this->ProfilerData[$key]['prefix_special']);
}
@@ -1154,8 +1167,13 @@
/**
* Generates report
*
+ * @param bool $returnResult
+ * @param bool $clean_output_buffer
+ *
+ * @return string
+ * @access public
*/
- function printReport($returnResult = false, $clean_output_buffer = true)
+ public function printReport($returnResult = false, $clean_output_buffer = true)
{
if ($this->reportDone) {
// don't print same report twice (in case if shutdown function used + compression + fatal error)
@@ -1192,7 +1210,7 @@
$this->appendHTML($this->highlightString($this->print_r($this->ProfilePoints, true)));
/*foreach ($this->ProfilePoints as $point => $locations) {
- foreach ($locations as $location => $occurences) {
+ foreach ($locations as $location => $occurrences) {
}
@@ -1338,6 +1356,8 @@
$this->reportDone = true;
}
+
+ return '';
}
/**
@@ -1402,13 +1422,16 @@
/**
* User-defined error handler
*
+ * @throws Exception
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
* @param array $errcontext
+ * @return bool
+ * @access public
*/
- function saveError($errno, $errstr, $errfile = '', $errline = '', $errcontext = '')
+ public function saveError($errno, $errstr, $errfile = null, $errline = null, $errcontext = Array ())
{
$this->ProfilerData['error_handling']['begins'] = memory_get_usage();
@@ -1425,7 +1448,7 @@
}
if ( DebuggerUtil::constOn('DBG_IGNORE_STRICT_ERRORS') && defined('E_STRICT') && ($errno == E_STRICT) ) {
- return ;
+ return false;
}
$this->expandError($errstr, $errfile, $errline);
@@ -1446,14 +1469,18 @@
// append debugger report to data in buffer & clean buffer afterwards
die( $this->breakOutofBuffering(false) . $this->printReport(true) );
}
+
+ return true;
}
/**
* User-defined exception handler
*
- * @param Exception $errno
+ * @param Exception $exception
+ * @return void
+ * @access public
*/
- function saveException($exception)
+ public function saveException($exception)
{
$this->ProfilerData['error_handling']['begins'] = memory_get_usage();
@@ -1534,9 +1561,16 @@
$this->appendHTML($ret);
}
- function AttachToApplication() {
- if (!DebuggerUtil::constOn('DBG_HANDLE_ERRORS')) {
- return true;
+ /**
+ * Attaches debugger to Application
+ *
+ * @return void
+ * @access public
+ */
+ public function AttachToApplication()
+ {
+ if ( !DebuggerUtil::constOn('DBG_HANDLE_ERRORS') ) {
+ return;
}
if ( class_exists('kApplication') ) {
@@ -1551,8 +1585,8 @@
$this->Application->exceptionHandlers[] = Array (&$this, 'saveException');
}
else {
- set_error_handler( Array(&$this, 'saveError') );
- set_exception_handler( Array(&$this, 'saveException') );
+ set_error_handler( Array (&$this, 'saveError') );
+ set_exception_handler( Array (&$this, 'saveException') );
}
}
Index: units/admin/admin_events_handler.php
===================================================================
--- units/admin/admin_events_handler.php (revision 14590)
+++ units/admin/admin_events_handler.php (working copy)
@@ -35,11 +35,13 @@
}
/**
- * Checks permissions of user
+ * Checks user permission to execute given $event
*
* @param kEvent $event
+ * @return bool
+ * @access public
*/
- function CheckPermission(&$event)
+ public function CheckPermission(&$event)
{
$perm_value = null;
@@ -99,7 +101,14 @@
$this->Conn->Query('DELETE FROM ' . TABLE_PREFIX . 'CachedUrls');
}
- function OnResetSections(&$event)
+ /**
+ * Resets tree section cache and refreshes admin section tree
+ *
+ * @param kEvent $event
+ * @return void
+ * @access protected
+ */
+ protected function OnResetSections(&$event)
{
if ($this->Application->GetVar('ajax') == 'yes') {
$event->status = kEvent::erSTOP;
@@ -585,14 +594,15 @@
function OnCSVImportStep(&$event)
{
$import_helper =& $this->Application->recallObject('CSVHelper');
- /* @var $export_helper kCSVHelper */
+ /* @var $import_helper kCSVHelper */
$prefix_special = $import_helper->ImportData('prefix');
$prefix_elems = preg_split('/\.|_/', $prefix_special, 2);
$perm_sections = $this->Application->getUnitOption($prefix_elems[0], 'PermSection');
- if(!$this->Application->CheckPermission($perm_sections['main'].'.add') && !$this->Application->CheckPermission($perm_sections['main'].'.edit')) {
+
+ if ( !$this->Application->CheckPermission($perm_sections['main'] . '.add') && !$this->Application->CheckPermission($perm_sections['main'] . '.edit') ) {
$event->status = kEvent::erPERM_FAIL;
- return ;
+ return;
}
$import_helper->ImportStep();
@@ -1080,13 +1090,20 @@
}
- function runSchemaText($sql)
+ /**
+ * Run given schema sqls and return error, if any
+ *
+ * @param $sql
+ * @return string
+ * @access protected
+ */
+ protected function runSchemaText($sql)
{
- $table_prefix = 'restore'.TABLE_PREFIX;
-// $table_prefix = TABLE_PREFIX;
+ $table_prefix = 'restore' . TABLE_PREFIX;
- if (strlen($table_prefix) > 0) {
+ if ( strlen($table_prefix) > 0 ) {
$replacements = Array ('INSERT INTO ', 'UPDATE ', 'ALTER TABLE ', 'DELETE FROM ', 'REPLACE INTO ');
+
foreach ($replacements as $replacement) {
$sql = str_replace($replacement, $replacement . $table_prefix, $sql);
}
@@ -1095,45 +1112,44 @@
$sql = str_replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS ' . $table_prefix, $sql);
$sql = str_replace('DROP TABLE ', 'DROP TABLE IF EXISTS ' . $table_prefix, $sql);
- $commands = explode("# --------------------------------------------------------",$sql);
- if(count($commands)>0)
- {
-// $query_func = getConnectionInterface('query',$dbo_type);
-// $errorno_func = getConnectionInterface('errorno',$dbo_type);
-// $errormsg_func = getConnectionInterface('errormsg',$dbo_type);
+ $commands = explode("# --------------------------------------------------------", $sql);
- for($i = 0; $i < count($commands); $i++)
- {
- $cmd = $commands[$i];
- $cmd = trim($cmd);
- if(strlen($cmd)>0)
- {
- $this->Conn->Query($cmd);
- if($this->Conn->errorCode != 0)
- {
- return $this->Conn->errorMessage." COMMAND:<PRE>$cmd</PRE>";
- }
- }
- }
- }
+ if ( count($commands) > 0 ) {
+ for ($i = 0; $i < count($commands); $i++) {
+ $cmd = trim( $commands[$i] );
+
+ if ( strlen($cmd) > 0 ) {
+ $this->Conn->Query($cmd);
+
+ if ( $this->Conn->hasError() ) {
+ return $this->Conn->getErrorMsg() . " COMMAND:<PRE>$cmd</PRE>";
+ }
+ }
+ }
+ }
+
+ return '';
}
- function runSQLText($allsql)
+ /**
+ * Runs given sqls and return error message, if any
+ *
+ * @param $all_sqls
+ * @return string
+ * @access protected
+ */
+ protected function runSQLText($all_sqls)
{
$line = 0;
-// $query_func = getConnectionInterface('query',$dbo_type);
-// $errorno_func = getConnectionInterface('errorno',$dbo_type);
-// $errormsg_func = getConnectionInterface('errormsg',$dbo_type);
- while($line<count($allsql))
- {
- $sql = $allsql[$line];
- if(strlen(trim($sql))>0 && substr($sql,0,1)!="#")
- {
- $table_prefix = 'restore'.TABLE_PREFIX;
+ while ( $line < count($all_sqls) ) {
+ $sql = $all_sqls[$line];
+ if ( strlen(trim($sql)) > 0 && substr($sql, 0, 1) != "#" ) {
+ $table_prefix = 'restore' . TABLE_PREFIX;
- if (strlen($table_prefix) > 0) {
+ if ( strlen($table_prefix) > 0 ) {
$replacements = Array ('INSERT INTO ', 'UPDATE ', 'ALTER TABLE ', 'DELETE FROM ', 'REPLACE INTO ');
+
foreach ($replacements as $replacement) {
$sql = str_replace($replacement, $replacement . $table_prefix, $sql);
}
@@ -1141,23 +1157,23 @@
$sql = str_replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS ' . $table_prefix, $sql);
$sql = str_replace('DROP TABLE ', 'DROP TABLE IF EXISTS ' . $table_prefix, $sql);
+ $sql = trim($sql);
- $sql = trim($sql);
- if(strlen($sql)>0)
- {
+ if ( strlen($sql) > 0 ) {
$this->Conn->Query($sql);
- if($this->Conn->errorCode != 0)
- {
- return $this->Conn->errorMessage." COMMAND:<PRE>$sql</PRE>";
- }
+ if ( $this->Conn->hasError() ) {
+ return $this->Conn->getErrorMsg() . " COMMAND:<PRE>$sql</PRE>";
+ }
}
}
+
$line++;
}
+
+ return '';
}
-
/**
* Starts restore process
*
|