anewt-developers team mailing list archive
-
anewt-developers team
-
Mailing list archive
-
Message #00113
[Branch ~uws/anewt/anewt.uws] Rev 1708: [database] Implement AnewtDatabaseConnectionMemcache
------------------------------------------------------------
revno: 1708
committer: Wouter Bolsterlee <uws@xxxxxxxxx>
branch nick: anewt.uws
timestamp: Sun 2009-07-26 16:32:43 +0200
message:
[database] Implement AnewtDatabaseConnectionMemcache
AnewtDatabaseConnectionMemcache is a database connection
wrapper with Memcache result set caching. It is a wrapper
around another database connection, providing a simple
caching solution for \c SELECT queries.
AnewtDatabaseConnectionMemcache includes tests and
documentation. See the docs for more information.
added:
database/backend-memcache.lib.php
database/test/memcache.test.php
modified:
database/database.lib.php
--
lp:anewt
https://code.launchpad.net/~uws/anewt/anewt.uws
Your team Anewt developers is subscribed to branch lp:anewt.
To unsubscribe from this branch go to https://code.launchpad.net/~uws/anewt/anewt.uws/+edit-subscription.
=== added file 'database/backend-memcache.lib.php'
--- database/backend-memcache.lib.php 1970-01-01 00:00:00 +0000
+++ database/backend-memcache.lib.php 2009-07-26 14:32:43 +0000
@@ -0,0 +1,313 @@
+<?php
+
+/*
+ * Anewt, Almost No Effort Web Toolkit, database module
+ *
+ * This code is copyrighted and distributed under the terms of the GNU LGPL.
+ * See the README file for more information.
+ */
+
+
+/**
+ * Database connection wrapper with Memcache result set caching.
+ *
+ * AnewtDatabaseConnectionMemcache is a wrapper around another database
+ * connection, providing a simple caching solution for \c SELECT queries. It is
+ * not supposed to be a fully featured database caching solution, but designed
+ * to work transparently in simple cases. Using AnewtDatabaseConnectionMemcache
+ * does not require any code changes, except for the database connection step.
+ * Just invoke AnewtDatabase::setup_connection() twice: one for the real
+ * connection, one for the cached connection.
+ *
+ * Note that connect(), disconnect() and is_connected() only apply to the
+ * Memcache server connection, and do not propagate to the underlying database
+ * connection. Since connections to the Memcache server are only established if
+ * required, it usually does not make sense to connect() and disconnect() the
+ * connection manually.
+ *
+ * How does it work? The rows returned from a \c SELECT query executed against
+ * a real database connection are temporarily stored in a Memcache cache. If the
+ * same query is executed again within a certain timespan (see
+ * <code>expiry</code> below), the exact same result is returned without hitting
+ * the database again. Note that this means that changes to the database while
+ * the data is in the cache are <strong>not immediately visible</strong>.
+ *
+ * Result row caching only works if you execute your \c SELECT queries through
+ * one of these convenience functions:
+ *
+ * - prepare_execute_fetch_one()
+ * - prepare_executev_fetch_one()
+ * - prepare_execute_fetch_all()
+ * - prepare_executev_fetch_all()
+ *
+ * Note that preparing and executing a query manually, i.e. by calling
+ * AnewtDatabaseConnection::prepare(), executing the returned
+ * AnewtDatabasePreparedQuery and then fetching rows with
+ * AnewtDatabaseResultSet::fetch_one() (or other fetch functions) will
+ * <strong>not</strong> result in caching!
+ *
+ * <strong>Important note:</strong> if you change the contents of the database
+ * and want to see the results of e.g. \c INSERT queries reflected immediately
+ * if you perform a \c SELECT query right after the \c INSERT, you should not
+ * rely on AnewtDatabaseConnectionMemcache, but interact directly with the real
+ * AnewtDatabaseConnection, e.g. AnewtDatabaseConnectionMySQL. You can use
+ * both the AnewtDatabaseConnectionMemcache connection and the underlying
+ * AnewtDatabaseConnection at the same time. This way, you can opt to use
+ * the cache for some of your \c SELECT queries (when delays are not an issue),
+ * while you deliberately avoid it for other \c SELECT queries (if you want
+ * changes to be immediately visible).
+ *
+ * Additionally, you can also delete all cached result sets by invoking
+ * AnewtDatabaseConnectionMemcache::flush_cache() directly.
+ *
+ * The settings accepted by this backend are:
+ *
+ * - <code>connection</code>: The existing AnewtDatabaseConnection to wrap
+ * - <code>hostname</code>: The hostname of the memcache server (optional,
+ * defaults to <code>localhost</code>)
+ * - <code>port</code>: The port number of the memcache server (optional,
+ * defaults to <code>11211</code>)
+ * - <code>socket</code>: Path to a Unix domain socket (optional). Instead
+ * of a network connection, you can also provide
+ * a path to a Unix socket where \c memcached
+ * listens. If this is specified, \c hostname and \c
+ * port are ignored.
+ * - <code>expiry</code>: The number of seconds to cache result sets
+ * (optional, defaults to <code>600</code>)
+ * - <code>identifier</code>: An application-specific identifier (optional).
+ * Use this to avoid collisions if two instances of
+ * the same application use the same Memcache server
+ * but you do not want them to share their cached
+ * values. In this case, you should specify an \c
+ * identifier value that is unique to this
+ * application instance.
+ * - <code>compression</code>: Whether to enable compression (optional, defaults
+ * to <code>false</code>)
+ *
+ * \see AnewtDatabase
+ * \see AnewtDatabaseConnection
+ */
+final class AnewtDatabaseConnectionMemcache extends AnewtDatabaseConnection
+{
+ /** The Memcache instance */
+ private $memcache;
+
+ /** Whether we are Memcache server is connected */
+ private $memcache_connected;
+
+ /** The number of Memcache hits */
+ public $n_cache_hits = 0;
+
+ /** The number of Memcache misses */
+ public $n_cache_misses = 0;
+
+ public function __construct($settings)
+ {
+ $default_settings = array(
+ 'connection' => null,
+ 'hostname' => 'localhost',
+ 'port' => 11211,
+ 'socket' => null,
+ 'expiry' => 600,
+ 'identifier' => 'anewt-database',
+ 'compression' => false,
+ );
+
+ parent::__construct(array_merge($default_settings, $settings));
+
+ if (!$this->settings['connection'] instanceof AnewtDatabaseConnection)
+ throw new AnewtDatabaseConnectionException('Connection is not a valid AnewtDatabaseConnection.');
+
+ $this->connection_handle = $this->settings['connection'];
+ $this->memcache = new Memcache();
+ }
+
+ /* Connection methods */
+
+ protected function real_connect()
+ {
+ $socket = $this->settings['socket'];
+ if (!is_null($socket))
+ {
+ $this->memcache_connected = $this->memcache->addServer(
+ sprintf('unix://%s', $socket),
+ 0,
+ $this->settings['persistent']
+ );
+ }
+ else
+ {
+ $this->memcache_connected = $this->memcache->addServer(
+ $this->settings['hostname'],
+ $this->settings['port'],
+ $this->settings['persistent']
+ );
+ }
+ }
+
+ protected function real_disconnect()
+ {
+ $this->memcache_connected = !$this->memcache->close(); /* Yes, negated */
+ }
+
+ public function is_connected()
+ {
+ return $this->memcache_connected;
+ }
+
+ public function last_insert_id($options=null)
+ {
+ return $this->connection_handle->last_insert_id($options);
+ }
+
+ function real_execute_sql($sql)
+ {
+ return $this->connection_handle->execute_sql($sql);
+ }
+
+
+ /* Caching query methods */
+
+ /**
+ * Construct a key to be used as a memcache key.
+ *
+ * \param $sql The SQL query
+ * \param $values The values for the SQL query
+ * \param $all_rows Whether this key is intended for single row or multiple
+ * row caching (boolean)
+ *
+ * \return A key that can be used as a memcache key.
+ */
+ private function build_key($sql, $values, $all_rows)
+ {
+ return sprintf(
+ '%s-%s-%s',
+ $this->settings['identifier'],
+ md5($sql . serialize($values)),
+ $all_rows ? 'all' : 'one'
+ );
+ }
+
+ public function prepare_executev_fetch_one($sql, $values=null)
+ {
+ $key = null;
+ $store_in_cache = false;
+
+ if (AnewtDatabaseSQLTemplate::query_type_for_sql($sql) == ANEWT_DATABASE_SQL_QUERY_TYPE_SELECT)
+ {
+ $key = $this->build_key($sql, $values, false);
+ $row = $this->memcache->get($key);
+
+ if ($row === false)
+ {
+ $this->n_cache_misses++;
+ $store_in_cache = true;
+ }
+ else
+ {
+ $this->n_cache_hits++;
+ return $row;
+ }
+ }
+
+ $row = $this->connection_handle->prepare_executev_fetch_one($sql, $values);
+
+ if ($store_in_cache)
+ $this->memcache->set(
+ $key,
+ $row,
+ $this->settings['compression'] ? MEMCACHE_COMPRESSED : 0,
+ $this->settings['expiry']
+ );
+
+ return $row;
+ }
+
+ public function prepare_executev_fetch_all($sql, $values=null)
+ {
+ $key = null;
+ $store_in_cache = false;
+
+ if (AnewtDatabaseSQLTemplate::query_type_for_sql($sql) == ANEWT_DATABASE_SQL_QUERY_TYPE_SELECT)
+ {
+ $key = $this->build_key($sql, $values, true);
+ $rows = $this->memcache->get($key);
+
+ if ($rows === false)
+ {
+ $this->n_cache_misses++;
+ $store_in_cache = true;
+ }
+ else
+ {
+ $this->n_cache_hits++;
+ return $rows;
+ }
+ }
+
+ $rows = $this->connection_handle->prepare_executev_fetch_all($sql, $values);
+
+ if ($store_in_cache)
+ $this->memcache->set(
+ $key,
+ $rows,
+ $this->settings['compression'] ? MEMCACHE_COMPRESSED : 0,
+ $this->settings['expiry']
+ );
+
+ return $rows;
+ }
+
+ /**
+ * Flush the contents of the query cache.
+ *
+ * This method can be used to flush the contents of the query cache. Note
+ * that this will <strong>delete all cached data</strong> from the Memcache
+ * server, i.e. including any data that has not been set from this
+ * connection!
+ */
+ public function flush_cache()
+ {
+ $this->memcache->flush();
+ }
+
+
+ /* Escaping methods */
+
+ function escape_boolean($value)
+ {
+ return $this->connection_handle->escape_boolean($value);
+ }
+
+ function escape_string($value)
+ {
+ return $this->connection_handle->escape_string($value);
+ }
+
+ function escape_table_name($value)
+ {
+ return $this->connection_handle->escape_table_name($value);
+ }
+
+ function escape_column_name($value)
+ {
+ return $this->connection_handle->escape_column_name($value);
+ }
+
+ function escape_date($value)
+ {
+ return $this->connection_handle->escape_date($value);
+ }
+
+ function escape_time($value)
+ {
+ return $this->connection_handle->escape_time($value);
+ }
+
+ function escape_datetime($value)
+ {
+ return $this->connection_handle->escape_datetime($value);
+ }
+}
+
+?>
=== modified file 'database/database.lib.php'
--- database/database.lib.php 2009-07-18 16:39:42 +0000
+++ database/database.lib.php 2009-07-26 14:32:43 +0000
@@ -140,6 +140,11 @@
$connection = new AnewtDatabaseConnectionPostgreSQL($settings);
break;
+ case 'memcache':
+ anewt_include('database/backend-memcache');
+ $connection = new AnewtDatabaseConnectionMemcache($settings);
+ break;
+
default:
throw new AnewtDatabaseException('Database type "%s" is not supported', $connection_type);
break;
=== added file 'database/test/memcache.test.php'
--- database/test/memcache.test.php 1970-01-01 00:00:00 +0000
+++ database/test/memcache.test.php 2009-07-26 14:32:43 +0000
@@ -0,0 +1,90 @@
+<?php
+
+anewt_include('database');
+
+class AnewtDatabaseMemcacheTestSuite extends PHPUnit_Framework_TestSuite
+{
+ public static function suite()
+ {
+ $s = new AnewtDatabaseMemcacheTestSuite();
+ $s->addTestSuite('AnewtDatabaseMemcacheTest');
+ return $s;
+ }
+
+ public function setup()
+ {
+ /* The real, underlying connection... */
+ $settings = array(
+ 'type' => 'sqlite',
+ );
+ AnewtDatabase::setup_connection($settings, 'non-cached');
+
+ /* ...and the caching connection */
+ $settings = array(
+ 'type' => 'memcache',
+ 'connection' => AnewtDatabase::get_connection('non-cached'),
+ 'expiry' => 2, /* Extremely short expiry time for testing purposes */
+ 'identifier' => 'test-id',
+ );
+ AnewtDatabase::setup_connection($settings, 'cached');
+ }
+}
+
+
+class AnewtDatabaseMemcacheTest extends PHPUnit_Framework_TestCase
+{
+ function xxxtest_connection()
+ {
+ $connection = AnewtDatabase::get_connection('cached');
+ $this->assertTrue($connection->is_connected());
+ $connection->disconnect();
+ $this->assertFalse($connection->is_connected());
+ $connection->connect();
+ $this->assertTrue($connection->is_connected());
+
+ $this->assertEquals(0, $connection->n_cache_hits);
+ $this->assertEquals(0, $connection->n_cache_misses);
+ }
+
+ function test_caching()
+ {
+ $connection = AnewtDatabase::get_connection('cached');
+
+ /* This should not not hit the cache */
+ $row = $connection->prepare('SELECT 1 AS test;')->execute()->fetch_one();
+ $this->assertEquals(1, $row['test']);
+
+ $this->assertEquals(0, $connection->n_cache_hits);
+ $this->assertEquals(0, $connection->n_cache_misses);
+
+ /* This should hit the cache the second time */
+ $rows = $connection->prepare_execute_fetch_all('SELECT 1 AS test;');
+ $this->assertEquals(1, count($rows));
+ $this->assertEquals(1, $rows[0]['test']);
+ $this->assertEquals(0, $connection->n_cache_hits);
+ $this->assertEquals(1, $connection->n_cache_misses);
+ $rows = $connection->prepare_execute_fetch_all('SELECT 1 AS test;');
+ $this->assertEquals(1, count($rows));
+ $this->assertEquals(1, $rows[0]['test']);
+ $this->assertEquals(1, $connection->n_cache_hits);
+ $this->assertEquals(1, $connection->n_cache_misses);
+
+ /* This should hit the cache the second time */
+ $row = $connection->prepare_execute_fetch_one('SELECT 1 AS test;');
+ $this->assertEquals(1, $row['test']);
+ $this->assertEquals(1, $connection->n_cache_hits);
+ $this->assertEquals(2, $connection->n_cache_misses);
+ $row = $connection->prepare_execute_fetch_one('SELECT 1 AS test;');
+ $this->assertEquals(1, $row['test']);
+ $this->assertEquals(2, $connection->n_cache_hits);
+ $this->assertEquals(2, $connection->n_cache_misses);
+
+ /* Flush the cache */
+ $connection->flush_cache();
+ $row = $connection->prepare_execute_fetch_one('SELECT 1 AS test;');
+ $this->assertEquals(2, $connection->n_cache_hits);
+ $this->assertEquals(3, $connection->n_cache_misses);
+ }
+}
+
+?>