Permalink
Browse files

Fixes 801687 - Upgrade bouncer database handling

* Implements PDO instead of old mysql_* API
* Adds error handling, raises exceptions on failure
* Adds new tests for new database layer which all pass (YAY!)
* Fixes older tests that rely on old SDO database object.
* Creates mock Memcache object and interface for caching layer.
  • Loading branch information...
1 parent ab31d81 commit 245c44c1bfb9879066f7037a57665fb5ff376904 @brandonsavage brandonsavage committed Mar 26, 2013
@@ -43,8 +43,8 @@ function getRegionFromIP($sdo, $ip) {
geoip_ip_to_country AS ip
ON
ip.country_code = cty.country_code AND
- ip_end = ( SELECT MIN(ip_end) FROM geoip_ip_to_country WHERE ip_end >= INET_ATON('%s') LIMIT 1 ) AND
- ip_start <= INET_ATON('%s')
+ ip_end = ( SELECT MIN(ip_end) FROM geoip_ip_to_country WHERE ip_end >= INET_ATON(?) LIMIT 1 ) AND
+ ip_start <= INET_ATON(?)
",array($ip, $ip));
if ($region)
@@ -66,7 +66,7 @@ function throttleGeoIPRegion($sdo, $region_id) {
FROM
geoip_regions
WHERE
- id = %d
+ id = ?
",array($region_id));
$region_throttle = $region_throttle['throttle'];
@@ -98,7 +98,7 @@ function getFallbackRegion($sdo, $region_id) {
FROM
geoip_regions
WHERE
- id = %d
+ id = ?
",array($region_id));
if($fallback) {
@@ -116,7 +116,7 @@ function getGlobalFallbackProhibited($sdo, $region_id) {
FROM
geoip_regions
WHERE
- id = %d
+ id = ?
",array($region_id));
if($fallback) {
@@ -140,7 +140,7 @@ function queryForMirrors($sdo, $http_type, $where_lang, $location_id, $client_re
// If we are using GEOIP, we need to customize the SQL accordingly.
if($client_region) {
- $cr_sql = ' geoip_mirror_region_map.region_id = %d AND ';
+ $cr_sql = ' geoip_mirror_region_map.region_id = ? AND ';
$arguments[] = $client_region;
} else {
$cr_sql = null;
@@ -167,19 +167,18 @@ function queryForMirrors($sdo, $http_type, $where_lang, $location_id, $client_re
JOIN
mirror_location_mirror_map ON mirror_mirrors.id = mirror_location_mirror_map.mirror_id
LEFT JOIN
- mirror_lmm_lang_exceptions AS lang_exc ON (mirror_location_mirror_map.id = lang_exc.location_mirror_map_id AND NOT lang_exc.language = '%s')
+ mirror_lmm_lang_exceptions AS lang_exc ON (mirror_location_mirror_map.id = lang_exc.location_mirror_map_id AND NOT lang_exc.language = ?)
INNER JOIN
geoip_mirror_region_map ON (geoip_mirror_region_map.mirror_id = mirror_mirrors.id)
WHERE
- mirror_location_mirror_map.location_id = %d AND
+ mirror_location_mirror_map.location_id = ? AND
$cr_sql
mirror_mirrors.active='1' AND
mirror_location_mirror_map.active ='1' AND
- mirror_location_mirror_map.healthy = '%d' AND
- mirror_mirrors.baseurl LIKE '$http_type%%'
+ mirror_location_mirror_map.healthy = ? AND
+ mirror_mirrors.baseurl LIKE '$http_type%'
ORDER BY rating",
- $arguments, MYSQL_ASSOC, 'id');
-
+ $arguments, SDO2::FETCH_NAMED, 'id');
// If we found no mirrors and we are not in the second execution of this
// function, let's try finding some unhealthy mirrors.
if(!$mirrors && !$recurse) {
@@ -188,3 +187,16 @@ function queryForMirrors($sdo, $http_type, $where_lang, $location_id, $client_re
return $mirrors;
}
+
+/**
+ * Get an ID based on name.
+ * @param string $table
+ * @param string $id_col
+ * @param string $name_col
+ * @param string $name
+ */
+function name_to_id($sdo, $table,$id_col,$name_col,$name)
+{
+ $buf = $sdo->get_one("SELECT {$id_col} FROM {$table} WHERE {$name_col} = ?", array($name), SDO2::FETCH_NUM);
+ return $buf[0];
+}
View
@@ -5,7 +5,6 @@
* @subpackage pub
*/
require_once('./cfg/config.php'); // config file that defines constants
-
require_once('./functions.php'); // The functions
// if we don't have an os, make it windows, playing the odds
@@ -46,21 +45,29 @@
header('Location: ' . $redirect_url);
exit;
}
-
- require_once(LIB.'/sdo.php');
-
- $sdo = new SDO();
+
+ require_once(LIB.'/sdo2.php');
+ require_once(LIB.'/memcaching.php');
+ $mc = new Memcaching();
+
+ $dbwrite = array(
+ 'host' => DBHOST,
+ 'name' => DBNAME,
+ 'user' => DBUSER,
+ 'pass' => DBPASS,
+ );
+ $sdo = new SDO2($mc, $dbwrite);
// get OS ID
- $os_id = $sdo->name_to_id('mirror_os','id','name',$os_name);
+ $os_id = name_to_id($sdo, 'mirror_os','id','name',$os_name);
// get product for this language (if applicable)
$buf = $sdo->get_one("
SELECT prod.id, prod.ssl_only FROM mirror_products AS prod
LEFT JOIN mirror_product_langs AS langs ON (prod.id = langs.product_id)
- WHERE prod.name LIKE '%s'
- AND (langs.language LIKE '%s' OR langs.language IS NULL)",
- array($product_name, $where_lang), MYSQL_ASSOC);
+ WHERE prod.name LIKE ?
+ AND (langs.language LIKE ? OR langs.language IS NULL)",
+ array($product_name, $where_lang), SDO2::FETCH_ASSOC);
if (!empty($buf['id'])) {
$product_id = $buf['id'];
$ssl_only = $buf['ssl_only'];
@@ -77,8 +84,8 @@
FROM
mirror_locations
WHERE
- product_id = %d AND
- os_id = %d", array($product_id, $os_id));
+ product_id = ? AND
+ os_id = ?", array($product_id, $os_id));
$client_ip = null;
$fallback_global = FALSE; // False means we WILL fall back.
@@ -149,8 +156,8 @@
// if logging is enabled, insert log
if (LOGGING) {
- $sdo->query("UPDATE mirror_mirrors SET count=count+1 WHERE id = %d",array($mirror['id']),false);
- $sdo->query("UPDATE mirror_products SET count=count+1 WHERE id = %d",array($product_id),false);
+ $sdo->query("UPDATE mirror_mirrors SET count=count+1 WHERE id = ?",array($mirror['id']),false);
+ $sdo->query("UPDATE mirror_products SET count=count+1 WHERE id = ?",array($product_id),false);
}
// replace :lang placeholder with requested language
@@ -37,11 +37,13 @@
*
* ***** END LICENSE BLOCK ***** */
+interface Memcache_Interface {}
+
/**
* This model is an interface to Memcache.
* It's called Memcaching to not interfere with the actual Memcache class.
*/
-class Memcaching {
+class Memcaching implements Memcache_Interface {
var $cache; // holds the memcache object
var $memcacheConnected; // did we find a valid memcache server?
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Scalable Data Object 2
+ *
+ * Replacement to use modern OO standards for PHP 5.x.
+ *
+ * @package mirror
+ * @subpackage lib
+ */
+
+require_once(LIB . '/memcaching.php');
+
+class SDO2 {
+
+ // We should abstract PDO away from the functions in our app that use this class.
+ // So we redefine the constants as a passthrough to PDO.
+ const FETCH_ASSOC = PDO::FETCH_ASSOC;
+ const FETCH_BOTH = PDO::FETCH_BOTH;
+ const FETCH_NAMED = PDO::FETCH_NAMED;
+ const FETCH_NUM = PDO::FETCH_NUM;
+
+ /**
+ * Database resource.
+ */
+ protected $db;
+
+ /**
+ * Memcache object.
+ * Requires use of Memcaching object.
+ */
+ protected $mc;
+
+ protected $db_write;
+ protected $db_read;
+ protected $db_details;
+
+ public function __construct(Memcache_Interface $mc, array $dbwrite = array(), array $dbread = array()) {
+ $this->mc = $mc;
+ if(empty($dbwrite) && empty($dbread)) {
+ throw new Exception('No credentials supplied for database connection');
+ }
+
+ if(empty($dbwrite)) {
+ throw new Exception('Write database credentials are required');
+ }
+
+ $this->db_details = array (
+ 'db_write' => array (
+ 'host' => $dbwrite['host'],
+ 'name' => $dbwrite['name'],
+ 'user' => $dbwrite['user'],
+ 'pass' => $dbwrite['pass'],
+ ),
+ 'db_read' => array (
+ 'host' => isset($dbread['host']) ? $dbread['host'] : $dbwrite['host'],
+ 'name' => isset($dbread['name']) ? $dbread['name'] : $dbwrite['name'],
+ 'user' => isset($dbread['user']) ? $dbread['user'] : $dbwrite['user'],
+ 'pass' => isset($dbread['pass']) ? $dbread['pass'] : $dbwrite['pass'],
+ ),
+ );
+ }
+
+ /**
+ * Connect to a MySQL database server.
+ * @param string $dbtype db selector (db_write,db_read)
+ * @return resource|boolean
+ */
+ public function connect($dbtype='db_write')
+ {
+ if(isset($this->$dbtype) && $this->$dbtype instanceof PDO) {
+ return $this->$dbtype;
+ }
+
+ $details = $this->db_details[$dbtype];
+ $dsn = 'mysql:host=' . $details['host'] . ';dbname=' . $details['name'];
+
+ try {
+ $this->$dbtype = new PDO($dsn, $details['user'], $details['pass'], array(PDO::ATTR_PERSISTENT => true));
+ $this->$dbtype->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ return $this->$dbtype;
+ } catch (Exception $e) {
+ /**
+ * If we can't connect at all then we have to show a fallback page w/
+ * the correct error code.
+ */
+ error_log($e->getMessage());
+ header('HTTP/1.0 500 Internal Server Error');
+ require_once(FILEPATH.'/gonefishing.php');
+ exit;
+ }
+ }
+
+ /**
+ * Execute a MySQL query.
+ * @param string $q MySQL query
+ * @param array $args arguments
+ * @param boolean $readonly query from read only database?
+ */
+ public function query($q, array $args=array(), $readonly=true)
+ {
+ $stmt = $this->build_query($q, $readonly);
+ $stmt->execute($args);
+ return $stmt;
+ }
+
+ /**
+ * Fetch a row as an array from a mysql result.
+ * @param string $result
+ * @return array
+ */
+ public function fetch_array(PDOStatement $result, $type = self::FETCH_BOTH)
+ {
+ return $result->fetchAll($type);
+ }
+
+ /**
+ * Fetch an array based on a query.
+ * @param string $query database query
+ * @param array $args query arguments
+ * @param int $type result type
+ * @param string $col_id if passed it, the values of this column in the result set will be used as the array keys in the returned array
+ * @return array $list array of database rows
+ *
+ * Example of returned array:
+ * <code>
+ * $this->get("SELECT * FROM table WHERE foo=?",array('bar'),MYSQL_ASSOC);
+ * returns...
+ * Array
+ * (
+ * [0] => Array
+ * (
+ * [id] => 1
+ * [field1] => data1
+ * [field2] => data2
+ * )
+ *
+ * )
+ * </code>
+ */
+ public function get($query,$args, $type = self::FETCH_NAMED, $col_id = null)
+ {
+
+ // trim our query
+ $query = trim($query);
+
+ // set cachekey
+ $cachekey = md5($this->flatten_query($query, $args).$type.$col_id);
+
+ // only return cached results if we have a valid cache object and the
+ // current query is a select query
+ if ($this->mc && 0 === strpos(strtolower($query), 'select')) {
+ if ($list = $this->mc->get($cachekey)) {
+ return $list;
+ }
+ }
+
+ $res = $this->query($query, $args);
+
+ $list = array();
+
+
+ if( $res instanceof PDOStatement &&
+ !is_null($col_id) &&
+ $type == self::FETCH_NAMED &&
+ $res->rowCount() > 0
+ ) {
+
+ $rows = $this->fetch_array($res, $type);
+
+ foreach($rows as $row) {
+ $list[$row[$col_id]] = $row;
+ }
+
+ return $list;
+ }
+
+ $list = $this->fetch_array($res, $type);
+
+ if ($this->mc && 0 === strpos(strtolower($query), 'select')) {
+ $this->mc->set($cachekey, $list);
+ }
+
+ return $list;
+ }
+
+ /**
+ * Get one record.
+ * @param string $query query
+ * @param int $type result type
+ */
+ public function get_one($query, $args, $type = self::FETCH_ASSOC) {
+ $buf = $this->query($query.' LIMIT 1',$args);
+ return $buf->fetch($type);
+ }
+
+ /**
+ * Parse pre-query and add passed args.
+ * @param string $base base query with ? marking variables
+ * @param array $args array containing args for substitution
+ */
+ public function build_query($base, $readonly) {
+ if($readonly) {
+ $dbh = $this->connect('db_read');
+ } else {
+ $dbh = $this->connect();
+ }
+
+ return $dbh->prepare($base);
+ }
+
+ /**
+ * Generate a flat string for a cache id.
+ * @param string $base base query
+ * @param array $args array containing args
+ * @return string
+ */
+ function flatten_query($base, $args) {
+ return $base.serialize($args);
+ }
+
+}
Oops, something went wrong.

0 comments on commit 245c44c

Please sign in to comment.