From 9b4644091103f53ccae1fdca63002b75101412e9 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Sat, 11 Jun 2016 02:33:12 -0400 Subject: [PATCH 1/2] Add db file to .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 11c60e3f..7aa979e3 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ wordpress/wp-content/* !wordpress/wp-content/themes/ !wordpress/wp-content/advanced-cache.php !wordpress/wp-content/index.php +!wordpress/wp-content/db.php # ignore these plugins wordpress/wp-content/plugins/hello.php -- GitLab From 7bae4098e93a0440d0ea1ddf856a199eed36fd50 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Sat, 11 Jun 2016 04:08:41 -0400 Subject: [PATCH 2/2] Add read replica support. --- wordpress/db-config.php | 389 ++++++++++++ wordpress/wp-config.default.php | 11 + wordpress/wp-content/db.php | 1012 +++++++++++++++++++++++++++++++ 3 files changed, 1412 insertions(+) create mode 100644 wordpress/db-config.php create mode 100644 wordpress/wp-content/db.php diff --git a/wordpress/db-config.php b/wordpress/db-config.php new file mode 100644 index 00000000..0f7202ab --- /dev/null +++ b/wordpress/db-config.php @@ -0,0 +1,389 @@ +queries. It is not + * a constant because you might want to use it momentarily. + * Default: false + */ +$wpdb->save_queries = false; + +/** + * persistent (bool) + * This determines whether to use mysql_connect or mysql_pconnect. The effects + * of this setting may vary and should be carefully tested. + * Default: false + */ +$wpdb->persistent = false; + +/** + * max_connections (int) + * This is the number of mysql connections to keep open. Increase if you expect + * to reuse a lot of connections to different servers. This is ignored if you + * enable persistent connections. + * Default: 10 + */ +$wpdb->max_connections = 10; + +/** + * check_tcp_responsiveness + * Enables checking TCP responsiveness by fsockopen prior to mysql_connect or + * mysql_pconnect. This was added because PHP's mysql functions do not provide + * a variable timeout setting. Disabling it may improve average performance by + * a very tiny margin but lose protection against connections failing slowly. + * Default: true + */ +$wpdb->check_tcp_responsiveness = true; + +/** Configuration Functions **/ + +/** + * $wpdb->add_database( $database ); + * + * $database is an associative array with these parameters: + * host (required) Hostname with optional :port. Default port is 3306. + * user (required) MySQL user name. + * password (required) MySQL user password. + * name (required) MySQL database name. + * read (optional) Whether server is readable. Default is 1 (readable). + * Also used to assign preference. See "Network topology". + * write (optional) Whether server is writable. Default is 1 (writable). + * Also used to assign preference in multi-master mode. + * dataset (optional) Name of dataset. Default is 'global'. + * timeout (optional) Seconds to wait for TCP responsiveness. Default is 0.2 + * lag_threshold (optional) The minimum lag on a slave in seconds before we consider it lagged. + * Set null to disable. When not set, the value of $wpdb->default_lag_threshold is used. + */ + +/** + * $wpdb->add_table( $dataset, $table ); + * + * $dataset and $table are strings. + */ + +/** + * $wpdb->add_callback( $callback, $callback_group = 'dataset' ); + * + * $callback is a callable function or method. $callback_group is the + * group of callbacks, this $callback belongs to. + * + * Callbacks are executed in the order in which they are registered until one + * of them returns something other than null. + * + * The default $callback_group is 'dataset'. Callback in this group + * will be called with two arguments and expected to compute a dataset or return null. + * $dataset = $callback($table, &$wpdb); + * + * Anything evaluating to false will cause the query to be aborted. + * + * For more complex setups, the callback may be used to overwrite properties of + * $wpdb or variables within hyperdb::connect_db(). If a callback returns an + * array, HyperDB will extract the array. It should be an associative array and + * it should include a $dataset value corresponding to a database added with + * $wpdb->add_database(). It may also include $server, which will be extracted + * to overwrite the parameters of each randomly selected database server prior + * to connection. This allows you to dynamically vary parameters such as the + * host, user, password, database name, lag_threshold and TCP check timeout. + */ + +/** Masters and slaves + * + * A database definition can include 'read' and 'write' parameters. These + * operate as boolean switches but they are typically specified as integers. + * They allow or disallow use of the database for reading or writing. + * + * A master database might be configured to allow reading and writing: + * 'write' => 1, + * 'read' => 1, + * while a slave would be allowed only to read: + * 'write' => 0, + * 'read' => 1, + * + * It might be advantageous to disallow reading from the master, such as when + * there are many slaves available and the master is very busy with writes. + * 'write' => 1, + * 'read' => 0, + * HyperDB tracks the tables that it has written since instantiation and sending + * subsequent read queries to the same server that received the write query. + * Thus a master set up this way will still receive read queries, but only + * subsequent to writes. + */ + + +/** + * Network topology / Datacenter awareness + * + * When your databases are located in separate physical locations there is + * typically an advantage to connecting to a nearby server instead of a more + * distant one. The read and write parameters can be used to place servers into + * logical groups of more or less preferred connections. Lower numbers indicate + * greater preference. + * + * This configuration instructs HyperDB to try reading from one of the local + * slaves at random. If that slave is unreachable or refuses the connection, + * the other slave will be tried, followed by the master, and finally the + * remote slaves in random order. + * Local slave 1: 'write' => 0, 'read' => 1, + * Local slave 2: 'write' => 0, 'read' => 1, + * Local master: 'write' => 1, 'read' => 2, + * Remote slave 1: 'write' => 0, 'read' => 3, + * Remote slave 2: 'write' => 0, 'read' => 3, + * + * In the other datacenter, the master would be remote. We would take that into + * account while deciding where to send reads. Writes would always be sent to + * the master, regardless of proximity. + * Local slave 1: 'write' => 0, 'read' => 1, + * Local slave 2: 'write' => 0, 'read' => 1, + * Remote slave 1: 'write' => 0, 'read' => 2, + * Remote slave 2: 'write' => 0, 'read' => 2, + * Remote master: 'write' => 1, 'read' => 3, + * + * There are many ways to achieve different configurations in different + * locations. You can deploy different config files. You can write code to + * discover the web server's location, such as by inspecting $_SERVER or + * php_uname(), and compute the read/write parameters accordingly. An example + * appears later in this file using the legacy function add_db_server(). + */ + +/** + * Slaves lag awareness + * + * HyperDB accommodates slave lag by making decisions, based on the defined lag + * threshold. If the lag threshold is not set, it will ignore the slave lag. + * Otherwise, it will try to find a non-lagged slave, before connecting to a lagged one. + * + * A slave is considered lagged, if it's replication lag is bigger than the lag threshold + * you have defined in $wpdb->$default_lag_threshold or in the per-database settings, using + * add_database(). You can also rewrite the lag threshold, by returning + * $server['lag_threshold'] variable with the 'dataset' group callbacks. + * + * HyperDB does not check the lag on the slaves. You have to define two callbacks + * callbacks to do that: + * + * $wpdb->add_callback( $callback, 'get_lag_cache' ); + * + * and + * + * $wpdb->add_callback( $callback, 'get_lag' ); + * + * The first one is called, before connecting to a slave and should return + * the replication lag in seconds or false, if unknown, based on $wpdb->lag_cache_key. + * + * The second callback is called after a connection to a slave is established. + * It should return it's replication lag or false, if unknown, + * based on the connection in $wpdb->dbhs[ $wpdb->dbhname ]. + */ + +/** Sample Configuration 1: Using the Default Server **/ +/** NOTE: THIS IS ACTIVE BY DEFAULT. COMMENT IT OUT. **/ + +/** + * This is the most basic way to add a server to HyperDB using only the + * required parameters: host, user, password, name. + * This adds the DB defined in wp-config.php as a read/write server for + * the 'global' dataset. (Every table is in 'global' by default.) + */ +$wpdb->add_database(array( + 'host' => DB_HOST, // If port is other than 3306, use host:port. + 'user' => DB_USER, + 'password' => DB_PASSWORD, + 'name' => DB_NAME, +)); + +/** + * This adds the same server again, only this time it is configured as a slave. + * The last three parameters are set to the defaults but are shown for clarity. + */ +$wpdb->add_database(array( + 'host' => DB_REPLICA_HOST, // If port is other than 3306, use host:port. + 'user' => DB_REPLICA_USER, + 'password' => DB_REPLICA_PASSWORD, + 'name' => DB_REPLICA_NAME, + 'write' => 0, + 'read' => 1, + 'dataset' => 'global', + 'timeout' => 0.2, +)); + +/** Sample Configuration 2: Partitioning **/ + +/** + * This example shows a setup where the multisite blog tables have been + * separated from the global dataset. + */ +/* +$wpdb->add_database(array( + 'host' => 'global.db.example.com', + 'user' => 'globaluser', + 'password' => 'globalpassword', + 'name' => 'globaldb', +)); +$wpdb->add_database(array( + 'host' => 'blog.db.example.com', + 'user' => 'bloguser', + 'password' => 'blogpassword', + 'name' => 'blogdb', + 'dataset' => 'blog', +)); +$wpdb->add_callback('my_db_callback'); +function my_db_callback($query, $wpdb) { + // Multisite blog tables are "{$base_prefix}{$blog_id}_*" + if ( preg_match("/^{$wpdb->base_prefix}\d+_/i", $wpdb->table) ) + return 'blog'; +} +*/ + + +/** Sample helper functions from WordPress.com **/ + +/** + * This is back-compatible with an older config style. It is for convenience. + * lhost, part, and dc were removed from hyperdb because the read and write + * parameters provide enough power to achieve the desired effects via config. + * + * @param string $dataset Datset: the name of the dataset. Just use "global" if you don't need horizontal partitioning. + * @param int $part Partition: the vertical partition number (1, 2, 3, etc.). Use "0" if you don't need vertical partitioning. + * @param string $dc Datacenter: where the database server is located. Airport codes are convenient. Use whatever. + * @param int $read Read group: tries all servers in lowest number group before trying higher number group. Typical: 1 for slaves, 2 for master. This will cause reads to go to slaves unless all slaves are unreachable. Zero for no reads. + * @param bool $write Write flag: is this server writable? Works the same as $read. Typical: 1 for master, 0 for slaves. + * @param string $host Internet address: host:port of server on internet. + * @param string $lhost Local address: host:port of server for use when in same datacenter. Leave empty if no local address exists. + * @param string $name Database name. + * @param string $user Database user. + * @param string $password Database password. + */ +/* +function add_db_server($dataset, $part, $dc, $read, $write, $host, $lhost, $name, $user, $password, $timeout = 0.2 ) { + global $wpdb; + + // dc is not used in hyperdb. This produces the desired effect of + // trying to connect to local servers before remote servers. Also + // increases time allowed for TCP responsiveness check. + if ( !empty($dc) && defined(DATACENTER) && $dc != DATACENTER ) { + if ( $read ) + $read += 10000; + if ( $write ) + $write += 10000; + $timeout = 0.7; + } + + // You'll need a hyperdb::add_callback() callback function to use partitioning. + // $wpdb->add_callback( 'my_func' ); + if ( $part ) + $dataset = $dataset . '_' . $part; + + $database = compact('dataset', 'read', 'write', 'host', 'name', 'user', 'password', 'timeout'); + + $wpdb->add_database($database); + + // lhost is not used in hyperdb. This configures hyperdb with an + // additional server to represent the local hostname so it tries to + // connect over the private interface before the public one. + if ( !empty( $lhost ) ) { + if ( $read ) + $database['read'] = $read - 0.5; + if ( $write ) + $database['write'] = $write - 0.5; + $wpdb->add_database( $database ); + } +} +*/ + +/** + * Sample replication lag detection configuration. + * + * We use mk-heartbeat (http://www.maatkit.org/doc/mk-heartbeat.html) + * to detect replication lag. + * + * This implementation requires the database user + * to have read access to the heartbeat table. + * + * The cache uses shared memory for portability. + * Can be modified to work with Memcached, APC and etc. + */ + +/* + +$wpdb->lag_cache_ttl = 30; +$wpdb->shmem_key = ftok( __FILE__, "Y" ); +$wpdb->shmem_size = 128 * 1024; + +$wpdb->add_callback( 'get_lag_cache', 'get_lag_cache' ); +$wpdb->add_callback( 'get_lag', 'get_lag' ); + +function get_lag_cache( $wpdb ) { + $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 ); + $lag_data = @shm_get_var( $segment, 0 ); + shm_detach( $segment ); + + if ( !is_array( $lag_data ) || !is_array( $lag_data[ $wpdb->lag_cache_key ] ) ) + return false; + + if ( $wpdb->lag_cache_ttl < time() - $lag_data[ $wpdb->lag_cache_key ][ 'timestamp' ] ) + return false; + + return $lag_data[ $wpdb->lag_cache_key ][ 'lag' ]; +} + +function get_lag( $wpdb ) { + $dbh = $wpdb->dbhs[ $wpdb->dbhname ]; + + if ( !mysql_select_db( 'heartbeat', $dbh ) ) + return false; + + $result = mysql_query( "SELECT UNIX_TIMESTAMP() - UNIX_TIMESTAMP(ts) AS lag FROM heartbeat LIMIT 1", $dbh ); + + if ( !$result || false === $row = mysql_fetch_assoc( $result ) ) + return false; + + // Cache the result in shared memory with timestamp + $sem_id = sem_get( $wpdb->shmem_key, 1, 0600, 1 ) ; + sem_acquire( $sem_id ); + $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 ); + $lag_data = @shm_get_var( $segment, 0 ); + + if ( !is_array( $lag_data ) ) + $lag_data = array(); + + $lag_data[ $wpdb->lag_cache_key ] = array( 'timestamp' => time(), 'lag' => $row[ 'lag' ] ); + shm_put_var( $segment, 0, $lag_data ); + shm_detach( $segment ); + sem_release( $sem_id ); + + return $row[ 'lag' ]; +} + +*/ + +// The ending PHP tag is omitted. This is actually safer than including it. diff --git a/wordpress/wp-config.default.php b/wordpress/wp-config.default.php index b827b155..5abe4fd4 100644 --- a/wordpress/wp-config.default.php +++ b/wordpress/wp-config.default.php @@ -37,6 +37,17 @@ define('DB_PASSWORD', $_SERVER['RDS_PASSWORD']); /** MySQL hostname */ define('DB_HOST', $_SERVER['RDS_HOSTNAME']); +define('DB_REPLICA_NAME', $_SERVER['RDS_REPLICA_DB_NAME']); + +/** MySQL database username */ +define('DB_REPLICA_USER', $_SERVER['RDS_REPLICA_USERNAME']); + +/** MySQL database password */ +define('DB_REPLICA_PASSWORD', $_SERVER['RDS_REPLICA_PASSWORD']); + +/** MySQL hostname */ +define('DB_REPLICA_HOST', $_SERVER['RDS_REPLICA_HOSTNAME']); + /** Database Charset to use in creating database tables. */ define('DB_CHARSET', 'utf8'); diff --git a/wordpress/wp-content/db.php b/wordpress/wp-content/db.php new file mode 100644 index 00000000..074031d6 --- /dev/null +++ b/wordpress/wp-content/db.php @@ -0,0 +1,1012 @@ + dbh) for established mysql connections + * @var array + */ + var $dbhs; + + /** + * The multi-dimensional array of datasets and servers + * @var array + */ + var $hyper_servers = array(); + + /** + * Optional directory of tables and their datasets + * @var array + */ + var $hyper_tables = array(); + + /** + * Optional directory of callbacks to determine datasets from queries + * @var array + */ + var $hyper_callbacks = array(); + + /** + * Custom callback to save debug info in $this->queries + * @var callable + */ + var $save_query_callback = null; + + /** + * Whether to use mysql_pconnect instead of mysql_connect + * @var bool + */ + var $persistent = false; + + /** + * The maximum number of db links to keep open. The least-recently used + * link will be closed when the number of links exceeds this. + * @var int + */ + var $max_connections = 10; + + /** + * Whether to check with fsockopen prior to mysql_connect. + * @var bool + */ + var $check_tcp_responsiveness = true; + + /** + * Minimum number of connections to try before bailing + * @var int + */ + var $min_tries = 3; + + /** + * Send Reads To Masters. This disables slave connections while true. + * Otherwise it is an array of written tables. + * @var array + */ + var $srtm = array(); + + /** + * The log of db connections made and the time each one took + * @var array + */ + var $db_connections; + + /** + * The list of unclosed connections sorted by LRU + */ + var $open_connections = array(); + + /** + * Lookup array (dbhname => host:port) + * @var array + */ + var $dbh2host = array(); + + /** + * The last server used and the database name selected + * @var array + */ + var $last_used_server; + + /** + * Lookup array (dbhname => (server, db name) ) for re-selecting the db + * when a link is re-used. + * @var array + */ + 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; + + /** + * Gets ready to make database connections + * @param array db class vars + */ + function __construct( $args = null ) { + if ( is_array($args) ) + foreach ( get_class_vars(__CLASS__) as $var => $value ) + if ( isset($args[$var]) ) + $this->$var = $args[$var]; + + $this->init_charset(); + } + + /** + * Triggers __construct() for backwards compatibility with PHP4 + */ + function hyperdb( $args = null ) { + return $this->__construct($args); + } + + /** + * 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; + } + + /** + * Add the connection parameters for a database + */ + function add_database( $db ) { + extract($db, EXTR_SKIP); + isset($dataset) or $dataset = 'global'; + isset($read) or $read = 1; + isset($write) or $write = 1; + unset($db['dataset']); + + if ( $read ) + $this->hyper_servers[ $dataset ][ 'read' ][ $read ][] = $db; + if ( $write ) + $this->hyper_servers[ $dataset ][ 'write' ][ $write ][] = $db; + } + + /** + * Specify the dateset where a table is found + */ + function add_table( $dataset, $table ) { + $this->hyper_tables[ $table ] = $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, $group = 'dataset' ) { + $this->hyper_callbacks[ $group ][] = $callback; + } + + /** + * Find the first table name referenced in a query + * @param string query + * @return string table + */ + function get_table_from_query ( $q ) { + // Remove characters that can legally trail the table name + $q = rtrim($q, ';/-#'); + // allow (select...) union [...] style queries. Use the first queries table name. + $q = ltrim($q, "\t ("); + + // Quickly match most common queries + if ( preg_match('/^\s*(?:' + . 'SELECT.*?\s+FROM' + . '|INSERT(?:\s+IGNORE)?(?:\s+INTO)?' + . '|REPLACE(?:\s+INTO)?' + . '|UPDATE(?:\s+IGNORE)?' + . '|DELETE(?:\s+IGNORE)?(?:\s+FROM)?' + . ')\s+`?([\w-]+)`?/is', $q, $maybe) ) + return $maybe[1]; + + // Refer to the previous query + if ( preg_match('/^\s*SELECT.*?\s+FOUND_ROWS\(\)/is', $q) ) + return $this->last_table; + + // SHOW TABLE STATUS and SHOW TABLES + if ( preg_match('/^\s*(?:' + . 'SHOW\s+TABLE\s+STATUS.+(?:LIKE\s+|WHERE\s+Name\s*=\s*)' + . '|SHOW\s+(?:FULL\s+)?TABLES.+(?:LIKE\s+|WHERE\s+Name\s*=\s*)' + . ')\W([\w-]+)\W/is', $q, $maybe) ) + return $maybe[1]; + + // Big pattern for the rest of the table-related queries in MySQL 5.0 + if ( preg_match('/^\s*(?:' + . '(?:EXPLAIN\s+(?:EXTENDED\s+)?)?SELECT.*?\s+FROM' + . '|INSERT(?:\s+LOW_PRIORITY|\s+DELAYED|\s+HIGH_PRIORITY)?(?:\s+IGNORE)?(?:\s+INTO)?' + . '|REPLACE(?:\s+LOW_PRIORITY|\s+DELAYED)?(?:\s+INTO)?' + . '|UPDATE(?:\s+LOW_PRIORITY)?(?:\s+IGNORE)?' + . '|DELETE(?:\s+LOW_PRIORITY|\s+QUICK|\s+IGNORE)*(?:\s+FROM)?' + . '|DESCRIBE|DESC|EXPLAIN|HANDLER' + . '|(?:LOCK|UNLOCK)\s+TABLE(?:S)?' + . '|(?:RENAME|OPTIMIZE|BACKUP|RESTORE|CHECK|CHECKSUM|ANALYZE|OPTIMIZE|REPAIR).*\s+TABLE' + . '|TRUNCATE(?:\s+TABLE)?' + . '|CREATE(?:\s+TEMPORARY)?\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?' + . '|ALTER(?:\s+IGNORE)?\s+TABLE' + . '|DROP\s+TABLE(?:\s+IF\s+EXISTS)?' + . '|CREATE(?:\s+\w+)?\s+INDEX.*\s+ON' + . '|DROP\s+INDEX.*\s+ON' + . '|LOAD\s+DATA.*INFILE.*INTO\s+TABLE' + . '|(?:GRANT|REVOKE).*ON\s+TABLE' + . '|SHOW\s+(?:.*FROM|.*TABLE)' + . ')\s+`?([\w-]+)`?/is', $q, $maybe) ) + return $maybe[1]; + } + + /** + * Determine the likelihood that this query could alter anything + * @param string query + * @return bool + */ + function is_write_query( $q ) { + // Quick and dirty: only SELECT statements are considered read-only. + $q = ltrim($q, "\r\n\t ("); + return !preg_match('/^(?:SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\s/i', $q); + } + + /** + * Set a flag to prevent reading from slaves which might be lagging after a write + */ + function send_reads_to_masters() { + $this->srtm = true; + } + + /** + * Callbacks are executed in the order in which they are registered until one + * of them returns something other than null. + */ + function run_callbacks( $group, $args = null) { + if ( !isset( $this->hyper_callbacks[ $group ] ) || !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; + } + } + + /** + * Figure out which database server should handle the query, and connect to it. + * @param string query + * @return resource mysql database connection + */ + function db_connect( $query = '' ) { + $connect_function = $this->persistent ? 'mysql_pconnect' : 'mysql_connect'; + + if ( empty( $query ) ) + return false; + + $this->last_table = $this->table = $this->get_table_from_query($query); + + 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( 'dataset', $query ) ) { + if ( is_array($this->callback_result) ) + extract( $this->callback_result, EXTR_OVERWRITE ); + else + $dataset = $this->callback_result; + } + + if ( ! isset($dataset) ) + $dataset = 'global'; + + if ( ! $dataset ) + return $this->bail("Unable to determine which dataset to query. ($this->table)"); + else + $this->dataset = $dataset; + + $this->run_callbacks( 'dataset_found', $dataset ); + + if ( empty( $this->hyper_servers ) ) { + if ( is_resource( $this->dbh ) ) + return $this->dbh; + if ( + !defined('DB_HOST') + || !defined('DB_USER') + || !defined('DB_PASSWORD') + || !defined('DB_NAME') ) + return $this->bail("We were unable to query because there was no database defined."); + $this->dbh = @ $connect_function(DB_HOST, DB_USER, DB_PASSWORD, true); + if ( ! is_resource( $this->dbh ) ) + return $this->bail("We were unable to connect to the database. (DB_HOST)"); + if ( ! mysql_select_db(DB_NAME, $this->dbh) ) + return $this->bail("We were unable to select the database."); + if ( ! empty( $this->charset ) ) { + $_collate = ! empty( $this->collate ) ? $this->collate : null; + $this->set_charset( $this->dbh, $this->charset, $_collate ); + } + + return $this->dbh; + } + + // Determine whether the query must be sent to the master (a writable server) + if ( !empty( $use_master ) || $this->srtm === true || isset($this->srtm[$this->table]) ) { + $use_master = true; + } elseif ( $is_write = $this->is_write_query($query) ) { + $use_master = true; + if ( is_array($this->srtm) ) + $this->srtm[$this->table] = true; + } elseif ( !isset($use_master) && is_array($this->srtm) && !empty($this->srtm) ) { + // Detect queries that have a join in the srtm array. + $use_master = false; + $query_match = substr( $query, 0, 1000 ); + foreach ( $this->srtm as $key => $value ) { + if ( false !== stripos( $query_match, $key ) ) { + $use_master = true; + break; + } + } + } else { + $use_master = false; + } + + if ( $use_master ) { + $this->dbhname = $dbhname = $dataset . '__w'; + $operation = 'write'; + } else { + $this->dbhname = $dbhname = $dataset . '__r'; + $operation = 'read'; + } + + // Try to reuse an existing connection + while ( isset( $this->dbhs[$dbhname] ) && is_resource( $this->dbhs[$dbhname] ) ) { + // Find the connection for incrementing counters + foreach ( array_keys($this->db_connections) as $i ) + if ( $this->db_connections[$i]['dbhname'] == $dbhname ) + $conn =& $this->db_connections[$i]; + + if ( isset($server['name']) ) { + $name = $server['name']; + // A callback has specified a database name so it's possible the existing connection selected a different one. + if ( $name != $this->used_servers[$dbhname]['name'] ) { + if ( !mysql_select_db($name, $this->dbhs[$dbhname]) ) { + // this can happen when the user varies and lacks permission on the $name database + if ( isset( $conn['disconnect (select failed)'] ) ) + ++$conn['disconnect (select failed)']; + else + $conn['disconnect (select failed)'] = 1; + + $this->disconnect($dbhname); + break; + } + $this->used_servers[$dbhname]['name'] = $name; + } + } else { + $name = $this->used_servers[$dbhname]['name']; + } + + $this->current_host = $this->dbh2host[$dbhname]; + + // Keep this connection at the top of the stack to prevent disconnecting frequently-used connections + if ( $k = array_search($dbhname, $this->open_connections) ) { + unset($this->open_connections[$k]); + $this->open_connections[] = $dbhname; + } + + $this->last_used_server = $this->used_servers[$dbhname]; + $this->last_connection = compact('dbhname', 'name'); + + if ( !mysql_ping($this->dbhs[$dbhname]) ) { + if ( isset( $conn['disconnect (ping failed)'] ) ) + ++$conn['disconnect (ping failed)']; + else + $conn['disconnect (ping failed)'] = 1; + + $this->disconnect($dbhname); + break; + } + + if ( isset( $conn['queries'] ) ) + ++$conn['queries']; + else + $conn['queries'] = 1; + + return $this->dbhs[$dbhname]; + } + + if ( $use_master && defined( "MASTER_DB_DEAD" ) ) { + return $this->bail("We're updating the database, please try back in 5 minutes. If you are posting to your blog please hit the refresh button on your browser in a few minutes to post the data again. It will be posted as soon as the database is back online again."); + } + + if ( empty($this->hyper_servers[$dataset][$operation]) ) + return $this->bail("No databases available with $this->table ($dataset)"); + + // Put the groups in order by priority + ksort($this->hyper_servers[$dataset][$operation]); + + // Make a list of at least $this->min_tries connections to try, repeating as necessary. + $servers = array(); + do { + foreach ( $this->hyper_servers[$dataset][$operation] as $group => $items ) { + $keys = array_keys($items); + shuffle($keys); + foreach ( $keys as $key ) + $servers[] = compact('group', 'key'); + } + + 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 + do { + $unique_lagged_slaves = array(); + $success = false; + + 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; + + // $group, $key + extract($group_key, EXTR_OVERWRITE); + + // $host, $user, $password, $name, $read, $write [, $lag_threshold, $connect_function, $timeout ] + extract($this->hyper_servers[$dataset][$operation][$group][$key], EXTR_OVERWRITE); + $port = null; + + // 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(); + + // 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(); + + 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', 'queries', 'lag'); + $this->db_connections[] = $this->last_connection; + $this->open_connections[] = $dbhname; + break; + } + } + + $success = false; + $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success'); + $this->db_connections[] = $this->last_connection; + $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, + 'port' => $port, + 'operation' => $operation, + 'table' => $this->table, + 'dataset' => $dataset, + 'dbhname' => $dbhname + ); + $this->run_callbacks( 'db_connection_error', $error_details ); + + return $this->bail( "Unable to connect to $host:$port to $operation table '$this->table' ($dataset)" ); + } + + break; + } while ( true ); + + if ( !isset( $charset ) ) + $charset = null; + + if ( !isset( $collate ) ) + $collate = null; + + $this->set_charset($this->dbhs[$dbhname], $charset, $collate); + + $this->dbh = $this->dbhs[$dbhname]; // needed by $wpdb->_real_escape() + + $this->last_used_server = compact('host', 'user', 'name', 'read', 'write'); + + $this->used_servers[$dbhname] = $this->last_used_server; + + while ( !$this->persistent && count($this->open_connections) > $this->max_connections ) { + $oldest_connection = array_shift($this->open_connections); + if ( $this->dbhs[$oldest_connection] != $this->dbhs[$dbhname] ) + $this->disconnect($oldest_connection); + } + + return $this->dbhs[$dbhname]; + } + + + /* + * Force addslashes() for the escapes. + * + * HyperDB makes connections when a query is made + * which is why we can't use mysql_real_escape_string() for escapes. + * This is also the reason why we don't allow certain charsets. See set_charset(). + */ + function _real_escape( $string ) { + return addslashes( $string ); + } + + /** + * 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 ( ! in_array( strtolower( $charset ), array( 'utf8', 'latin1' ) ) ) + wp_die( "$charset charset isn't supported in HyperDB for security reasons" ); + + 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 + */ + function disconnect($dbhname) { + if ( $k = array_search($dbhname, $this->open_connections) ) + unset($this->open_connections[$k]); + + if ( is_resource($this->dbhs[$dbhname]) ) + mysql_close($this->dbhs[$dbhname]); + + unset($this->dbhs[$dbhname]); + } + + /** + * Kill cached query results + */ + function flush() { + $this->last_error = ''; + $this->num_rows = 0; + parent::flush(); + } + + /** + * Basic query. See docs for more details. + * @param string $query + * @return int number of rows + */ + function query($query) { + // some queries are made before the plugins have been loaded, and thus cannot be filtered with this method + if ( function_exists('apply_filters') ) + $query = apply_filters('query', $query); + + // initialise return + $return_val = 0; + $this->flush(); + + // Log how the function was called + $this->func_call = "\$db->query(\"$query\")"; + + // Keep track of the last query for debug.. + $this->last_query = $query; + + if ( preg_match('/^\s*SELECT\s+FOUND_ROWS(\s*)/i', $query) && is_resource($this->last_found_rows_result) ) { + $this->result = $this->last_found_rows_result; + $elapsed = 0; + } else { + $this->dbh = $this->db_connect( $query ); + + if ( ! is_resource($this->dbh) ) + return false; + + $this->timer_start(); + $this->result = mysql_query($query, $this->dbh); + $elapsed = $this->timer_stop(); + ++$this->num_queries; + + if ( preg_match('/^\s*SELECT\s+SQL_CALC_FOUND_ROWS\s/i', $query) ) { + if ( false === strpos($query, "NO_SELECT_FOUND_ROWS") ) { + $this->timer_start(); + $this->last_found_rows_result = mysql_query("SELECT FOUND_ROWS()", $this->dbh); + $elapsed += $this->timer_stop(); + ++$this->num_queries; + $query .= "; SELECT FOUND_ROWS()"; + } + } else { + $this->last_found_rows_result = null; + } + + if ( $this->save_queries ) { + if ( is_callable($this->save_query_callback) ) + $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() ); + } + } + + // If there is an error then take note of it + if ( $this->last_error = mysql_error($this->dbh) ) { + $this->print_error($this->last_error); + return false; + } + + if ( preg_match("/^\\s*(insert|delete|update|replace|alter) /i",$query) ) { + $this->rows_affected = mysql_affected_rows($this->dbh); + + // Take note of the insert_id + if ( preg_match("/^\\s*(insert|replace) /i",$query) ) { + $this->insert_id = mysql_insert_id($this->dbh); + } + // Return number of rows affected + $return_val = $this->rows_affected; + } else { + $i = 0; + $this->col_info = array(); + while ($i < @mysql_num_fields($this->result)) { + $this->col_info[$i] = @mysql_fetch_field($this->result); + $i++; + } + $num_rows = 0; + $this->last_result = array(); + while ( $row = @mysql_fetch_object($this->result) ) { + $this->last_result[$num_rows] = $row; + $num_rows++; + } + + @mysql_free_result($this->result); + + // Log number of rows the query returned + $this->num_rows = $num_rows; + + // Return number of rows selected + $return_val = $this->num_rows; + } + + return $return_val; + } + + /** + * Whether or not MySQL database is at least the required minimum version. + * The additional argument allows the caller to check a specific database. + * + * @since 2.5.0 + * @uses $wp_version + * + * @return WP_Error + */ + function check_database_version( $dbh_or_table = false ) { + global $wp_version; + // Make sure the server has MySQL 4.1.2 + $mysql_version = preg_replace( '|[^0-9\.]|', '', $this->db_version( $dbh_or_table ) ); + if ( version_compare($mysql_version, '4.1.2', '<') ) + return new WP_Error( 'database_version', sprintf(__('ERROR: WordPress %s requires MySQL 4.1.2 or higher'), $wp_version) ); + } + + /** + * This function is called when WordPress is generating the table schema to determine wether or not the current database + * supports or needs the collation statements. + * The additional argument allows the caller to check a specific database. + * @return bool + */ + function supports_collation( $dbh_or_table = false ) { + return $this->has_cap( 'collation', $dbh_or_table ); + } + + /** + * Generic function to determine if a database supports a particular feature + * The additional argument allows the caller to check a specific database. + * @param string $db_cap the feature + * @param false|string|resource $dbh_or_table the databaese (the current database, the database housing the specified table, or the database of the mysql resource) + * @return bool + */ + function has_cap( $db_cap, $dbh_or_table = false ) { + $version = $this->db_version( $dbh_or_table ); + + switch ( strtolower( $db_cap ) ) : + case 'collation' : + case 'group_concat' : + case 'subqueries' : + return version_compare($version, '4.1', '>='); + case 'set_charset' : + return version_compare($version, '5.0.7', '>='); + endswitch; + + return false; + } + + /** + * The database version number + * @param false|string|resource $dbh_or_table the databaese (the current database, the database housing the specified table, or the database of the mysql resource) + * @return false|string false on failure, version number on success + */ + function db_version( $dbh_or_table = false ) { + if ( !$dbh_or_table && $this->dbh ) + $dbh =& $this->dbh; + elseif ( is_resource( $dbh_or_table ) ) + $dbh =& $dbh_or_table; + else + $dbh = $this->db_connect( "SELECT FROM $dbh_or_table $this->users" ); + + if ( $dbh ) + return preg_replace('/[^0-9.].*/', '', mysql_get_server_info( $dbh )); + return false; + } + + /** + * Get the name of the function that called wpdb. + * @return string the name of the calling function + */ + function get_caller() { + // requires PHP 4.3+ + if ( !is_callable('debug_backtrace') ) + return ''; + + $bt = debug_backtrace( false ); + $caller = ''; + + foreach ( (array) $bt as $trace ) { + if ( isset($trace['class']) && is_a( $this, $trace['class'] ) ) + continue; + elseif ( !isset($trace['function']) ) + continue; + elseif ( strtolower($trace['function']) == 'call_user_func_array' ) + continue; + elseif ( strtolower($trace['function']) == 'apply_filters' ) + continue; + elseif ( strtolower($trace['function']) == 'do_action' ) + continue; + + if ( isset($trace['class']) ) + $caller = $trace['class'] . '::' . $trace['function']; + else + $caller = $trace['function']; + break; + } + return $caller; + } + + /** + * Check the responsiveness of a tcp/ip daemon + * @return (bool) true when $host:$post responds within $float_timeout seconds, else (bool) false + */ + function check_tcp_responsiveness( $host, $port, $float_timeout ) { + if ( 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 ); + return "[ > $float_timeout ] ($errno) '$errstr'"; + } + + fclose( $socket ); + + if ( $use_apc ) + apc_store( $apc_key, 'up', $apc_ttl ); + + 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 + +} // class hyperdb + +$wpdb = new hyperdb(); + +require( DB_CONFIG_FILE ); + +?> -- GitLab