if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-class Memcached_DataObject extends DB_DataObject
+class Memcached_DataObject extends Safe_DataObject
{
- /**
- * Destructor to free global memory resources associated with
- * this data object when it's unset or goes out of scope.
- * DB_DataObject doesn't do this yet by itself.
- */
-
- function __destruct()
- {
- $this->free();
- if (method_exists('DB_DataObject', '__destruct')) {
- parent::__destruct();
- }
- }
-
- /**
- * Magic function called at serialize() time.
- *
- * We use this to drop a couple process-specific references
- * from DB_DataObject which can cause trouble in future
- * processes.
- *
- * @return array of variable names to include in serialization.
- */
- function __sleep()
- {
- $vars = array_keys(get_object_vars($this));
- $skip = array('_DB_resultid', '_link_loaded');
- return array_diff($vars, $skip);
- }
-
- /**
- * Magic function called at unserialize() time.
- *
- * Clean out some process-specific variables which might
- * be floating around from a previous process's cached
- * objects.
- *
- * Old cached objects may still have them.
- */
- function __wakeup()
- {
- // Refers to global state info from a previous process.
- // Clear this out so we don't accidentally break global
- // state in *this* process.
- $this->_DB_resultid = null;
-
- // We don't have any local DBO refs, so clear these out.
- $this->_link_loaded = false;
- }
-
/**
* Wrapper for DB_DataObject's static lookup using memcached
* as backing instead of an in-process cache array.
unset($i);
}
$i = Memcached_DataObject::getcached($cls, $k, $v);
- if ($i) {
- return $i;
- } else {
+ if ($i === false) { // false == cache miss
$i = DB_DataObject::factory($cls);
if (empty($i)) {
$i = false;
}
$result = $i->get($k, $v);
if ($result) {
+ // Hit!
$i->encache();
- return $i;
} else {
+ // save the fact that no such row exists
+ $c = self::memcache();
+ if (!empty($c)) {
+ $ck = self::cachekey($cls, $k, $v);
+ $c->set($ck, null);
+ }
$i = false;
- return $i;
}
}
+ return $i;
}
- function &pkeyGet($cls, $kv)
+ /**
+ * @fixme Should this return false on lookup fail to match staticGet?
+ */
+ function pkeyGet($cls, $kv)
{
$i = Memcached_DataObject::multicache($cls, $kv);
- if ($i) {
+ if ($i !== false) { // false == cache miss
return $i;
} else {
- $i = new $cls();
+ $i = DB_DataObject::factory($cls);
+ if (empty($i) || PEAR::isError($i)) {
+ return false;
+ }
foreach ($kv as $k => $v) {
$i->$k = $v;
}
$i->encache();
} else {
$i = null;
+ $c = self::memcache();
+ if (!empty($c)) {
+ $ck = self::multicacheKey($cls, $kv);
+ $c->set($ck, null);
+ }
}
return $i;
}
function insert()
{
$result = parent::insert();
+ if ($result) {
+ $this->fixupTimestamps();
+ $this->encache(); // in case of cached negative lookups
+ }
return $result;
}
}
$result = parent::update($orig);
if ($result) {
+ $this->fixupTimestamps();
$this->encache();
}
return $result;
}
static function memcache() {
- return common_memcache();
+ return Cache::instance();
}
static function cacheKey($cls, $k, $v) {
- if (is_object($cls) || is_object($k) || is_object($v)) {
+ if (is_object($cls) || is_object($k) || (is_object($v) && !($v instanceof DB_DataObject_Cast))) {
$e = new Exception();
common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
str_replace("\n", " ", $e->getTraceAsString()));
}
- return common_cache_key(strtolower($cls).':'.$k.':'.$v);
+ $vstr = self::valueString($v);
+ return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
}
static function getcached($cls, $k, $v) {
function keyTypes()
{
+ // ini-based classes return number-indexed arrays. handbuilt
+ // classes return column => keytype. Make this uniform.
+
+ $keys = $this->keys();
+
+ $keyskeys = array_keys($keys);
+
+ if (is_string($keyskeys[0])) {
+ return $keys;
+ }
+
global $_DB_DATAOBJECT;
if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
$this->databaseStructure();
function encache()
{
$c = $this->memcache();
+
if (!$c) {
return false;
} else if ($this->tableName() == 'user' && is_object($this->id)) {
str_replace("\n", " ", $e->getTraceAsString()));
return false;
} else {
- $pkey = array();
- $pval = array();
- $types = $this->keyTypes();
- ksort($types);
- foreach ($types as $key => $type) {
- if ($type == 'K') {
- $pkey[] = $key;
- $pval[] = $this->$key;
- } else {
- $c->set($this->cacheKey($this->tableName(), $key, $this->$key), $this);
- }
+ $keys = $this->_allCacheKeys();
+
+ foreach ($keys as $key) {
+ $c->set($key, $this);
}
- # XXX: should work for both compound and scalar pkeys
- $pvals = implode(',', $pval);
- $pkeys = implode(',', $pkey);
- $c->set($this->cacheKey($this->tableName(), $pkeys, $pvals), $this);
}
}
function decache()
{
$c = $this->memcache();
+
if (!$c) {
return false;
- } else {
- $pkey = array();
- $pval = array();
- $types = $this->keyTypes();
- ksort($types);
- foreach ($types as $key => $type) {
- if ($type == 'K') {
- $pkey[] = $key;
- $pval[] = $this->$key;
- } else {
- $c->delete($this->cacheKey($this->tableName(), $key, $this->$key));
+ }
+
+ $keys = $this->_allCacheKeys();
+
+ foreach ($keys as $key) {
+ $c->delete($key, $this);
+ }
+ }
+
+ function _allCacheKeys()
+ {
+ $ckeys = array();
+
+ $types = $this->keyTypes();
+ ksort($types);
+
+ $pkey = array();
+ $pval = array();
+
+ foreach ($types as $key => $type) {
+
+ assert(!empty($key));
+
+ if ($type == 'U') {
+ if (empty($this->$key)) {
+ continue;
}
+ $ckeys[] = $this->cacheKey($this->tableName(), $key, self::valueString($this->$key));
+ } else if ($type == 'K' || $type == 'N') {
+ $pkey[] = $key;
+ $pval[] = self::valueString($this->$key);
+ } else {
+ // Low level exception. No need for i18n as discussed with Brion.
+ throw new Exception("Unknown key type $key => $type for " . $this->tableName());
}
- # should work for both compound and scalar pkeys
- # XXX: comma works for now but may not be safe separator for future keys
- $pvals = implode(',', $pval);
- $pkeys = implode(',', $pkey);
- $c->delete($this->cacheKey($this->tableName(), $pkeys, $pvals));
}
+
+ assert(count($pkey) > 0);
+
+ // XXX: should work for both compound and scalar pkeys
+ $pvals = implode(',', $pval);
+ $pkeys = implode(',', $pkey);
+
+ $ckeys[] = $this->cacheKey($this->tableName(), $pkeys, $pvals);
+
+ return $ckeys;
}
function multicache($cls, $kv)
{
ksort($kv);
- $c = Memcached_DataObject::memcache();
+ $c = self::memcache();
if (!$c) {
return false;
} else {
- $pkeys = implode(',', array_keys($kv));
- $pvals = implode(',', array_values($kv));
- return $c->get(Memcached_DataObject::cacheKey($cls, $pkeys, $pvals));
+ return $c->get(self::multicacheKey($cls, $kv));
}
}
+ static function multicacheKey($cls, $kv)
+ {
+ ksort($kv);
+ $pkeys = implode(',', array_keys($kv));
+ $pvals = implode(',', array_values($kv));
+ return self::cacheKey($cls, $pkeys, $pvals);
+ }
+
function getSearchEngine($table)
{
require_once INSTALLDIR.'/lib/search_engines.php';
} else if ($type == 'fulltext') {
$search_engine = new MySQLSearch($this, $table);
} else {
+ // Low level exception. No need for i18n as discussed with Brion.
throw new ServerException('Unknown search type: ' . $type);
}
} else {
$inst->query($qry);
return $inst;
}
- $key_part = common_keyize($cls).':'.md5($qry);
- $ckey = common_cache_key($key_part);
+ $key_part = Cache::keyize($cls).':'.md5($qry);
+ $ckey = Cache::key($key_part);
$stored = $c->get($ckey);
- if ($stored) {
+
+ if ($stored !== false) {
return new ArrayWrapper($stored);
}
$cached[] = clone($inst);
}
$inst->free();
- $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry);
+ $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
return new ArrayWrapper($cached);
}
+ /**
+ * sends query to database - this is the private one that must work
+ * - internal functions use this rather than $this->query()
+ *
+ * Overridden to do logging.
+ *
+ * @param string $string
+ * @access private
+ * @return mixed none or PEAR_Error
+ */
+ function _query($string)
+ {
+ if (common_config('db', 'annotate_queries')) {
+ $string = $this->annotateQuery($string);
+ }
+
+ $start = microtime(true);
+ $fail = false;
+ $result = null;
+ if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
+ common_perf_counter('query', $string);
+ try {
+ $result = parent::_query($string);
+ } catch (Exception $e) {
+ $fail = $e;
+ }
+ Event::handle('EndDBQuery', array($this, $string, &$result));
+ }
+ $delta = microtime(true) - $start;
+
+ $limit = common_config('db', 'log_slow_queries');
+ if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
+ $clean = $this->sanitizeQuery($string);
+ if ($fail) {
+ $msg = sprintf("FAILED DB query (%0.3fs): %s - %s", $delta, $fail->getMessage(), $clean);
+ } else {
+ $msg = sprintf("DB query (%0.3fs): %s", $delta, $clean);
+ }
+ common_log(LOG_DEBUG, $msg);
+ }
+
+ if ($fail) {
+ throw $fail;
+ }
+ return $result;
+ }
+
+ /**
+ * Find the first caller in the stack trace that's not a
+ * low-level database function and add a comment to the
+ * query string. This should then be visible in process lists
+ * and slow query logs, to help identify problem areas.
+ *
+ * Also marks whether this was a web GET/POST or which daemon
+ * was running it.
+ *
+ * @param string $string SQL query string
+ * @return string SQL query string, with a comment in it
+ */
+ function annotateQuery($string)
+ {
+ $ignore = array('annotateQuery',
+ '_query',
+ 'query',
+ 'get',
+ 'insert',
+ 'delete',
+ 'update',
+ 'find');
+ $ignoreStatic = array('staticGet',
+ 'pkeyGet',
+ 'cachedQuery');
+ $here = get_class($this); // if we get confused
+ $bt = debug_backtrace();
+
+ // Find the first caller that's not us?
+ foreach ($bt as $frame) {
+ $func = $frame['function'];
+ if (isset($frame['type']) && $frame['type'] == '::') {
+ if (in_array($func, $ignoreStatic)) {
+ continue;
+ }
+ $here = $frame['class'] . '::' . $func;
+ break;
+ } else if (isset($frame['type']) && $frame['type'] == '->') {
+ if ($frame['object'] === $this && in_array($func, $ignore)) {
+ continue;
+ }
+ if (in_array($func, $ignoreStatic)) {
+ continue; // @fixme this shouldn't be needed?
+ }
+ $here = get_class($frame['object']) . '->' . $func;
+ break;
+ }
+ $here = $func;
+ break;
+ }
+
+ if (php_sapi_name() == 'cli') {
+ $context = basename($_SERVER['PHP_SELF']);
+ } else {
+ $context = $_SERVER['REQUEST_METHOD'];
+ }
+
+ // Slip the comment in after the first command,
+ // or DB_DataObject gets confused about handling inserts and such.
+ $parts = explode(' ', $string, 2);
+ $parts[0] .= " /* $context $here */";
+ return implode(' ', $parts);
+ }
+
+ // Sanitize a query for logging
+ // @fixme don't trim spaces in string literals
+ function sanitizeQuery($string)
+ {
+ $string = preg_replace('/\s+/', ' ', $string);
+ $string = trim($string);
+ return $string;
+ }
+
// We overload so that 'SET NAMES "utf8"' is called for
// each connection
//
// WARNING WARNING if we end up actually using multiple DBs at a time
// we'll need some fancier logic here.
- if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS'])) {
+ if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
if (!empty($conn)) {
$conn->disconnect();
}
}
}
+ // Needed to make timestamp values usefully comparable.
+ if (common_config('db', 'type') == 'mysql') {
+ parent::_query("set time_zone='+0:00'");
+ }
}
return $result;
}
if (!$dsn) {
- throw new Exception("No database name / dsn found anywhere");
+ // TRANS: Exception thrown when database name or Data Source Name could not be found.
+ throw new Exception(_("No database name or DSN found anywhere."));
}
return $dsn;
$keyPart = vsprintf($format, $args);
- $cacheKey = common_cache_key($keyPart);
+ $cacheKey = Cache::key($keyPart);
return $c->delete($cacheKey);
}
+
+ function fixupTimestamps()
+ {
+ // Fake up timestamp columns
+ $columns = $this->table();
+ foreach ($columns as $name => $type) {
+ if ($type & DB_DATAOBJECT_MYSQLTIMESTAMP) {
+ $this->$name = common_sql_now();
+ }
+ }
+ }
+
+ function debugDump()
+ {
+ common_debug("debugDump: " . common_log_objstring($this));
+ }
+
+ function raiseError($message, $type = null, $behaviour = null)
+ {
+ $id = get_class($this);
+ if (!empty($this->id)) {
+ $id .= ':' . $this->id;
+ }
+ if ($message instanceof PEAR_Error) {
+ $message = $message->getMessage();
+ }
+ // Low level exception. No need for i18n as discussed with Brion.
+ throw new ServerException("[$id] DB_DataObject error [$type]: $message");
+ }
+
+ static function cacheGet($keyPart)
+ {
+ $c = self::memcache();
+
+ if (empty($c)) {
+ return false;
+ }
+
+ $cacheKey = Cache::key($keyPart);
+
+ return $c->get($cacheKey);
+ }
+
+ static function cacheSet($keyPart, $value, $flag=null, $expiry=null)
+ {
+ $c = self::memcache();
+
+ if (empty($c)) {
+ return false;
+ }
+
+ $cacheKey = Cache::key($keyPart);
+
+ return $c->set($cacheKey, $value, $flag, $expiry);
+ }
+
+ static function valueString($v)
+ {
+ $vstr = null;
+ if (is_object($v) && $v instanceof DB_DataObject_Cast) {
+ switch ($v->type) {
+ case 'date':
+ $vstr = $v->year . '-' . $v->month . '-' . $v->day;
+ break;
+ case 'blob':
+ case 'string':
+ case 'sql':
+ case 'datetime':
+ case 'time':
+ // Low level exception. No need for i18n as discussed with Brion.
+ throw new ServerException("Unhandled DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
+ break;
+ default:
+ // Low level exception. No need for i18n as discussed with Brion.
+ throw new ServerException("Unknown DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
+ break;
+ }
+ } else {
+ $vstr = strval($v);
+ }
+ return $vstr;
+ }
}