diff --git a/wp-content/db.php b/wp-content/db.php index a1f61d4adaa804384eb980786fdeb01db181b8b5..c660ae8a97fd3749da23654a22e34e77ea455f2e 100644 --- a/wp-content/db.php +++ b/wp-content/db.php @@ -5,7 +5,10 @@ /** Load the wpdb class while preventing instantiation **/ $wpdb = true; -require_once( ABSPATH . WPINC . '/wp-db.php' ); +if ( defined('WPDB_PATH') ) + require_once(WPDB_PATH); +else + require_once( ABSPATH . WPINC . '/wp-db.php' ); if ( defined('DB_CONFIG_FILE') && file_exists( DB_CONFIG_FILE ) ) { @@ -29,6 +32,13 @@ if ( defined('DB_CONFIG_FILE') && file_exists( DB_CONFIG_FILE ) ) { } +/** + * Common definitions + */ +define( 'HYPERDB_LAG_OK', 1 ); +define( 'HYPERDB_LAG_BEHIND', 2 ); +define( 'HYPERDB_LAG_UNKNOWN', 3 ); + class hyperdb extends wpdb { /** * The last table that was queried @@ -144,10 +154,22 @@ class hyperdb extends wpdb { */ var $used_servers = array(); + /** + * Whether to save debug_backtrace in save_query_callback. You may wish + * to disable this, e.g. when tracing out-of-memory problems. + */ + var $save_backtrace = true; + + /** + * Maximum lag in seconds. Set null to disable. Requires callbacks. + * @var integer + */ + var $default_lag_threshold = null; + /** * Triggers __construct() for backwards compatibility with PHP4 */ - function db( $args = null ) { + function hyperdb( $args = null ) { return $this->__construct($args); } @@ -160,6 +182,26 @@ class hyperdb extends wpdb { foreach ( get_class_vars(__CLASS__) as $var => $value ) if ( isset($args[$var]) ) $this->$var = $args[$var]; + + $this->init_charset(); + } + + /** + * Sets $this->charset and $this->collate + */ + function init_charset() { + if ( function_exists('is_multisite') && is_multisite() ) { + $this->charset = 'utf8'; + if ( defined( 'DB_COLLATE' ) && DB_COLLATE ) + $this->collate = DB_COLLATE; + else + $this->collate = 'utf8_general_ci'; + } elseif ( defined( 'DB_COLLATE' ) ) { + $this->collate = DB_COLLATE; + } + + if ( defined( 'DB_CHARSET' ) ) + $this->charset = DB_CHARSET; } /** @@ -171,6 +213,7 @@ class hyperdb extends wpdb { isset($read) or $read = 1; isset($write) or $write = 1; unset($db['dataset']); + if ( $read ) $this->hyper_servers[ $dataset ][ 'read' ][ $read ][] = $db; if ( $write ) @@ -185,10 +228,12 @@ class hyperdb extends wpdb { } /** - * Add a callback to examine queries and determine dataset. + * Add a callback to a group of callbacks. + * The default group is 'dataset', used to examine + * queries and determine dataset. */ - function add_callback( $callback ) { - $this->hyper_callbacks[] = $callback; + function add_callback( $callback, $group = 'dataset' ) { + $this->hyper_callbacks[ $group ][] = $callback; } /** @@ -252,9 +297,8 @@ class hyperdb extends wpdb { */ function is_write_query( $q ) { // Quick and dirty: only SELECT statements are considered read-only. - $q = ltrim($q, "\t ("); - $word = strtoupper( substr( trim( $q ), 0, 6 ) ); - return 'SELECT' != $word; + $q = ltrim($q, "\r\n\t ("); + return !preg_match('/^(?:SELECT|SHOW|DESCRIBE|EXPLAIN)\s/i', $q); } /** @@ -265,12 +309,22 @@ class hyperdb extends wpdb { } /** - * Callbacks are specified in the config. They must return a dataset - * or an associative array with an element called 'dataset'. + * Callbacks are executed in the order in which they are registered until one + * of them returns something other than null. */ - function run_callbacks( $query ) { - $args = array($query, &$this); - foreach ( $this->hyper_callbacks as $func ) { + function run_callbacks( $group, $args = null) { + if ( !is_array( $this->hyper_callbacks[ $group ] ) ) + return null; + + if ( !isset($args) ) { + $args = array( &$this ); + } elseif ( is_array( $args ) ) { + $args[] = &$this; + } else { + $args = array( $args, &$this ); + } + + foreach ( $this->hyper_callbacks[ $group ] as $func ) { $result = call_user_func_array($func, $args); if ( isset($result) ) return $result; @@ -315,7 +369,7 @@ class hyperdb extends wpdb { if ( isset($this->hyper_tables[$this->table]) ) { $dataset = $this->hyper_tables[$this->table]; $this->callback_result = null; - } elseif ( null !== $this->callback_result = $this->run_callbacks($query) ) { + } elseif ( null !== $this->callback_result = $this->run_callbacks( 'dataset', $query ) ) { if ( is_array($this->callback_result) ) extract( $this->callback_result, EXTR_OVERWRITE ); else @@ -417,99 +471,176 @@ class hyperdb extends wpdb { foreach ( $this->hyper_servers[$dataset][$operation] as $group => $items ) { $keys = array_keys($items); shuffle($keys); - foreach ( $keys as $key ) { + foreach ( $keys as $key ) $servers[] = compact('group', 'key'); - } } - $tries_remaining = count($servers); - if ( !$tries_remaining ) + + if ( !$tries_remaining = count( $servers ) ) return $this->bail("No database servers were found to match the query. ($this->table, $dataset)"); + + if ( !isset( $unique_servers ) ) + $unique_servers = $tries_remaining; + } while ( $tries_remaining < $this->min_tries ); // Connect to a database server - foreach ( $servers as $group_key ) { - --$tries_remaining; - - // $group, $key - extract($group_key, EXTR_OVERWRITE); + do { + $unique_lagged_slaves = array(); + $success = false; - // $host, $user, $password, $name, $read, $write [ $connect_function, $timeout ] - extract($this->hyper_servers[$dataset][$operation][$group][$key], EXTR_OVERWRITE); + foreach ( $servers as $group_key ) { + --$tries_remaining; + + // If all servers are lagged, we need to start ignoring the lag and retry + if ( count( $unique_lagged_slaves ) == $unique_servers ) + break; - list($host, $port) = explode(':', $host); + // $group, $key + extract($group_key, EXTR_OVERWRITE); - // Split host:port into $host and $port - if ( strpos($host, ':') ) - list($host, $port) = explode(':', $host); + // $host, $user, $password, $name, $read, $write [, $lag_threshold, $connect_function, $timeout ] + extract($this->hyper_servers[$dataset][$operation][$group][$key], EXTR_OVERWRITE); - // Overlay $server if it was extracted from a callback - if ( isset($server) && is_array($server) ) - extract($server, EXTR_OVERWRITE); - - // Split again in case $server had host:port - if ( strpos($host, ':') ) list($host, $port) = explode(':', $host); - // Make sure there's always a port number - if ( empty($port) ) - $port = 3306; - - // Use a default timeout of 200ms - if ( !isset($timeout) ) - $timeout = 0.2; + // Split host:port into $host and $port + if ( strpos($host, ':') ) + list($host, $port) = explode(':', $host); + + // Overlay $server if it was extracted from a callback + if ( isset($server) && is_array($server) ) + extract($server, EXTR_OVERWRITE); + + // Split again in case $server had host:port + if ( strpos($host, ':') ) + list($host, $port) = explode(':', $host); + + // Make sure there's always a port number + if ( empty($port) ) + $port = 3306; + + // Use a default timeout of 200ms + if ( !isset($timeout) ) + $timeout = 0.2; + + // Get the minimum group here, in case $server rewrites it + if ( !isset( $min_group ) || $min_group > $group ) + $min_group = $group; + + // Can be used by the lag callbacks + $this->lag_cache_key = "$host:$port"; + $this->lag_threshold = isset($lag_threshold) ? $lag_threshold : $this->default_lag_threshold; + + // Check for a lagged slave, if applicable + if ( !$use_master && !$write && !isset( $ignore_slave_lag ) + && isset($this->lag_threshold) && !isset( $server['host'] ) + && ( $lagged_status = $this->get_lag_cache() ) === HYPERDB_LAG_BEHIND + ) { + // If it is the last lagged slave and it is with the best preference we will ignore its lag + if ( !isset( $unique_lagged_slaves[ "$host:$port" ] ) + && $unique_servers == count( $unique_lagged_slaves ) + 1 + && $group == $min_group ) + { + $this->lag_threshold = null; + } else { + $unique_lagged_slaves["$host.$port"] = $this->lag; + continue; + } + } - $this->timer_start(); + $this->timer_start(); - // Connect if necessary or possible - $tcp = null; - if ( $use_master || !$tries_remaining || !$this->check_tcp_responsiveness || true === $tcp = $this->check_tcp_responsiveness($host, $port, $timeout) ) { - $this->dbhs[$dbhname] = @ $connect_function( "$host:$port", $user, $password, true ); - } else { - $this->dbhs[$dbhname] = false; - } + // Connect if necessary or possible + $tcp = null; + if ( $use_master || !$tries_remaining || !$this->check_tcp_responsiveness + || true === $tcp = $this->check_tcp_responsiveness($host, $port, $timeout) ) + { + $this->dbhs[$dbhname] = @ $connect_function( "$host:$port", $user, $password, true ); + } else { + $this->dbhs[$dbhname] = false; + } - $elapsed = $this->timer_stop(); + $elapsed = $this->timer_stop(); + + if ( is_resource( $this->dbhs[$dbhname] ) ) { + /** + * If we care about lag, disconnect lagged slaves and try to find others. + * We don't disconnect if it is the last lagged slave and it is with the best preference. + */ + if ( !$use_master && !$write && !isset( $ignore_slave_lag ) + && isset($this->lag_threshold) && !isset( $server['host'] ) + && $lagged_status !== HYPERDB_LAG_OK + && ( $lagged_status = $this->get_lag() ) === HYPERDB_LAG_BEHIND + && !( + !isset( $unique_lagged_slaves[ "$host:$port" ] ) + && $unique_servers == count( $unique_lagged_slaves ) + 1 + && $group == $min_group + ) + ) { + $success = false; + $unique_lagged_slaves["$host:$port"] = $this->lag; + $this->disconnect( $dbhname ); + $this->dbhs[$dbhname] = false; + $msg = "Replication lag of {$this->lag}s on $host:$port ($dbhname)"; + $this->print_error( $msg ); + continue; + } elseif ( mysql_select_db( $name, $this->dbhs[ $dbhname ] ) ) { + $success = true; + $this->current_host = "$host:$port"; + $this->dbh2host[$dbhname] = "$host:$port"; + $queries = 1; + $lag = isset( $this->lag ) ? $this->lag : 0; + $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success', 'querie +s', 'lag'); + $this->db_connections[] = $this->last_connection; + $this->open_connections[] = $dbhname; + break; + } + } - if ( is_resource($this->dbhs[$dbhname]) && mysql_select_db( $name, $this->dbhs[$dbhname] ) ) { - $success = true; - $this->current_host = "$host:$port"; - $this->dbh2host[$dbhname] = "$host:$port"; - $queries = 1; - $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success', 'queries'); - $this->db_connections[] = $this->last_connection; - $this->open_connections[] = $dbhname; - break; - } else { $success = false; $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success'); $this->db_connections[] = $this->last_connection; - $error_details = array ( - 'referrer' => "{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}", -'server'=>$server, + + $msg = date( "Y-m-d H:i:s" ) . " Can't select $dbhname - \n"; + $msg .= "'referrer' => '{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}',\n"; + $msg .= "'server' => {$server},\n"; + $msg .= "'host' => {$host},\n"; + $msg .= "'error' => " . mysql_error() . ",\n"; + $msg .= "'errno' => " . mysql_errno() . ",\n"; + $msg .= "'tcp_responsive' => " . ( $tcp === true ? 'true' : $tcp ) . ",\n"; + $msg .= "'lagged_status' => " . ( isset( $lagged_status ) ? $lagged_status : HYPERDB_LAG_UNKNOWN ); + + $this->print_error( $msg ); + } + + if ( !$success || !isset($this->dbhs[$dbhname]) || !is_resource( $this->dbhs[$dbhname] ) ) { + if ( !isset( $ignore_slave_lag ) && count( $unique_lagged_slaves ) ) { + // Lagged slaves were not used. Ignore the lag for this connection attempt and retry. + $ignore_slave_lag = true; + $tries_remaining = count( $servers ); + continue; + } + + $error_details = array( 'host' => $host, - 'error' => mysql_error(), - 'errno' => mysql_errno(), - 'tcp_responsive' => $tcp === true ? 'true' : $tcp, + 'port' => $port, + 'operation' => $operation, + 'table' => $this->table, + 'dataset' => $dataset, + 'dbhname' => $dbhname ); - $msg = date( "Y-m-d H:i:s" ) . " Can't select $dbhname - "; - $msg .= "\n" . print_r($error_details, true); + $this->run_callbacks( 'db_connection_error', $error_details ); - $this->print_error( $msg ); + return $this->bail( "Unable to connect to $host:$port to $operation table '$this->table' ($dataset)" ); } - } - if ( ! is_resource( $this->dbhs[$dbhname] ) ) - return $this->bail("Unable to connect to $host:$port to $operation table '$this->table' ($dataset)"); + break; + } while ( true ); + + $this->set_charset($this->dbhs[$dbhname], $charset, $collate); - if ( !empty($charset) ) - $collation_query = "SET NAMES '$charset'"; - elseif ( !empty($this->charset) ) - $collation_query = "SET NAMES '$this->charset'"; - if ( !empty($collation_query) && !empty($collate) ) - $collation_query .= " COLLATE '$collate'"; - if ( !empty($collation_query) && !empty($this->collation) ) - $collation_query .= " COLLATE '$this->collation'"; - mysql_query($collation_query, $this->dbhs[$dbhname]); + $this->dbh = $this->dbhs[$dbhname]; // needed by $wpdb->_real_escape() $this->last_used_server = compact('host', 'user', 'name', 'read', 'write'); @@ -524,6 +655,30 @@ class hyperdb extends wpdb { return $this->dbhs[$dbhname]; } + /** + * Sets the connection's character set. + * @param resource $dbh The resource given by mysql_connect + * @param string $charset The character set (optional) + * @param string $collate The collation (optional) + */ + function set_charset($dbh, $charset = null, $collate = null) { + if ( !isset($charset) ) + $charset = $this->charset; + if ( !isset($collate) ) + $collate = $this->collate; + if ( $this->has_cap( 'collation', $dbh ) && !empty( $charset ) ) { + if ( function_exists( 'mysql_set_charset' ) && $this->has_cap( 'set_charset', $dbh ) ) { + mysql_set_charset( $charset, $dbh ); + $this->real_escape = true; + } else { + $query = $this->prepare( 'SET NAMES %s', $charset ); + if ( ! empty( $collate ) ) + $query .= $this->prepare( ' COLLATE %s', $collate ); + mysql_query( $query, $dbh ); + } + } + } + /** * Disconnect and remove connection from open connections list * @param string $tdbhname @@ -595,7 +750,7 @@ class hyperdb extends wpdb { if ( $this->save_queries ) { if ( is_callable($this->save_query_callback) ) - $this->queries[] = call_user_func_array( $this->save_query_callback, array( $query, $elapsed, debug_backtrace(), &$this ) ); + $this->queries[] = call_user_func_array( $this->save_query_callback, array( $query, $elapsed, $this->save_backtrace ? debug_backtrace( false ) : null, &$this ) ); else $this->queries[] = array( $query, $elapsed, $this->get_caller() ); } @@ -684,7 +839,8 @@ class hyperdb extends wpdb { case 'group_concat' : case 'subqueries' : return version_compare($version, '4.1', '>='); - break; + case 'set_charset' : + return version_compare($version, '5.0.7', '>='); endswitch; return false; @@ -717,7 +873,7 @@ class hyperdb extends wpdb { if ( !is_callable('debug_backtrace') ) return ''; - $bt = debug_backtrace(); + $bt = debug_backtrace( false ); $caller = ''; foreach ( (array) $bt as $trace ) { @@ -746,34 +902,33 @@ class hyperdb extends wpdb { * @return (bool) true when $host:$post responds within $float_timeout seconds, else (bool) false */ function check_tcp_responsiveness($host, $port, $float_timeout) { - if ( 1 == 2 && function_exists('apc_store') ) { - $use_apc = true; - $apc_key = "{$host}{$port}"; - $apc_ttl = 10; - } else { - $use_apc = false; - } - if ( $use_apc ) { - $cached_value=apc_fetch($apc_key); - switch ( $cached_value ) { - case 'up': - $this->tcp_responsive = 'true'; - return true; - case 'down': - $this->tcp_responsive = 'false'; - return false; - } - } - $socket = @ fsockopen($host, $port, $errno, $errstr, $float_timeout); - if ( $socket === false ) { - if ( $use_apc ) - apc_store($apc_key, 'down', $apc_ttl); + $socket = @ fsockopen($host, $port, $errno, $errstr, $float_timeout); + if ( $socket === false ) return "[ > $float_timeout ] ($errno) '$errstr'"; - } fclose($socket); - if ( $use_apc ) - apc_store($apc_key, 'up', $apc_ttl); - return true; + return true; + } + + function get_lag_cache() { + $this->lag = $this->run_callbacks( 'get_lag_cache' ); + + return $this->check_lag(); + } + + function get_lag() { + $this->lag = $this->run_callbacks( 'get_lag' ); + + return $this->check_lag(); + } + + function check_lag() { + if ( $this->lag === false ) + return HYPERDB_LAG_UNKNOWN; + + if ( $this->lag > $this->lag_threshold ) + return HYPERDB_LAG_BEHIND; + + return HYPERDB_LAG_OK; } // Helper functions for configuration