array(), 'ops' => array());
/**
* Place an item into memcache.
*
* @param string $key
* The string with which you will retrieve this item later.
* @param mixed $value
* The item to be stored.
* @param int $exp
* Parameter expire is expiration time in seconds. If it's 0, the item never
* expires (but memcached server doesn't guarantee this item to be stored all
* the time, it could be deleted from the cache to make place for other
* items).
* @param string $bin
* The name of the Drupal subsystem that is making this call. Examples could
* be 'cache', 'alias', 'taxonomy term' etc. It is possible to map different
* $bin values to different memcache servers.
* @param object $mc
* Optionally pass in the memcache object. Normally this value is determined
* automatically based on the bin the object is being stored to.
*
* @return bool
* TRUE on succes, FALSE otherwise.
*/
function dmemcache_set($key, $value, $exp = 0, $bin = 'cache', $mc = NULL) {
$collect_stats = dmemcache_stats_init();
$full_key = dmemcache_key($key, $bin);
$rc = FALSE;
if ($mc || ($mc = dmemcache_object($bin))) {
if ($mc instanceof Memcached) {
$rc = $mc->set($full_key, $value, $exp);
}
else {
$rc = $mc->set($full_key, $value, MEMCACHE_COMPRESSED, $exp);
}
}
if ($collect_stats) {
dmemcache_stats_write('set', $bin, array($full_key => $rc));
}
return $rc;
}
/**
* Add an item into memcache.
*
* @param string $key
* The string with which you will retrieve this item later.
* @param mixed $value
* The item to be stored.
* @param int $exp
* Parameter expire is expiration time in seconds. If it's 0, the
* item never expires (but memcached server doesn't guarantee this item to be
* stored all the time, it could be deleted from the cache to make place for
* other items).
* @param string $bin
* The name of the Drupal subsystem that is making this call.
* Examples could be 'cache', 'alias', 'taxonomy term' etc. It is possible
* to map different $bin values to different memcache servers.
* @param object $mc
* Optionally pass in the memcache object. Normally this value is
* determined automatically based on the bin the object is being stored to.
* @param int $flag
* If using the older memcache PECL extension as opposed to the
* newer memcached PECL extension, the MEMCACHE_COMPRESSED flag can be set
* to use zlib to store a compressed copy of the item. This flag option is
* completely ignored when using the newer memcached PECL extension.
*
* @return bool
* FALSE if placing the item into memcache failed.
*/
function dmemcache_add($key, $value, $exp = 0, $bin = 'cache', $mc = NULL, $flag = FALSE) {
$collect_stats = dmemcache_stats_init();
$full_key = dmemcache_key($key, $bin);
$rc = FALSE;
if ($mc || ($mc = dmemcache_object($bin))) {
if ($mc instanceof Memcached) {
$rc = $mc->add($full_key, $value, $exp);
}
else {
$rc = $mc->add($full_key, $value, $flag, $exp);
}
}
if ($collect_stats) {
dmemcache_stats_write('add', $bin, array($full_key => $rc));
}
return $rc;
}
/**
* Retrieve a value from the cache.
*
* @param string $key
* The key with which the item was stored.
* @param string $bin
* The bin in which the item was stored.
*
* @return mixed
* The item which was originally saved or FALSE
*/
function dmemcache_get($key, $bin = 'cache', $mc = NULL) {
$collect_stats = dmemcache_stats_init();
$result = FALSE;
$full_key = dmemcache_key($key, $bin);
if ($mc || $mc = dmemcache_object($bin)) {
$track_errors = ini_set('track_errors', '1');
$php_errormsg = '';
$result = @$mc->get($full_key);
if (!empty($php_errormsg)) {
register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
$php_errormsg = '';
}
ini_set('track_errors', $track_errors);
}
if ($collect_stats) {
dmemcache_stats_write('get', $bin, array($full_key => (bool) $result));
}
return $result;
}
/**
* Retrieve multiple values from the cache.
*
* @param array $keys
* The keys with which the items were stored.
* @param string $bin
* The bin in which the item was stored.
*
* @return mixed
* The item which was originally saved or FALSE
*/
function dmemcache_get_multi($keys, $bin = 'cache', $mc = NULL) {
$collect_stats = dmemcache_stats_init();
$multi_stats = array();
$full_keys = array();
foreach ($keys as $key => $cid) {
$full_key = dmemcache_key($cid, $bin);
$full_keys[$cid] = $full_key;
if ($collect_stats) {
$multi_stats[$full_key] = FALSE;
}
}
$results = array();
if ($mc || ($mc = dmemcache_object($bin))) {
if ($mc instanceof Memcached) {
$results = $mc->getMulti($full_keys);
}
elseif ($mc instanceof Memcache) {
$track_errors = ini_set('track_errors', '1');
$php_errormsg = '';
$results = @$mc->get($full_keys);
if (!empty($php_errormsg)) {
register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get_multi: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
$php_errormsg = '';
}
ini_set('track_errors', $track_errors);
}
}
// If $results is FALSE, convert it to an empty array.
if (!$results) {
$results = array();
}
if ($collect_stats) {
foreach ($multi_stats as $key => $value) {
$multi_stats[$key] = isset($results[$key]) ? TRUE : FALSE;
}
}
// Convert the full keys back to the cid.
$cid_results = array();
$cid_lookup = array_flip($full_keys);
foreach ($results as $key => $value) {
$cid_results[$cid_lookup[$key]] = $value;
}
if ($collect_stats) {
dmemcache_stats_write('getMulti', $bin, $multi_stats);
}
return $cid_results;
}
/**
* Deletes an item from the cache.
*
* @param string $key
* The key with which the item was stored.
* @param string $bin
* The bin in which the item was stored.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
function dmemcache_delete($key, $bin = 'cache', $mc = NULL) {
$collect_stats = dmemcache_stats_init();
$full_key = dmemcache_key($key, $bin);
$rc = FALSE;
if ($mc || ($mc = dmemcache_object($bin))) {
$rc = $mc->delete($full_key, 0);
}
if ($collect_stats) {
dmemcache_stats_write('delete', $bin, array($full_key => $rc));
}
return $rc;
}
/**
* Flush all stored items.
*
* Immediately invalidates all existing items. dmemcache_flush doesn't actually
* free any resources, it only marks all the items as expired, so occupied
* memory will be overwritten by new items.
*
* @param string $bin
* The bin to flush. Note that this will flush all bins mapped to the same
* server as $bin. There is no way at this time to empty just one bin.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
function dmemcache_flush($bin = 'cache', $mc = NULL) {
$collect_stats = dmemcache_stats_init();
$rc = FALSE;
if ($mc || ($mc = dmemcache_object($bin))) {
$rc = memcache_flush($mc);
}
if ($collect_stats) {
dmemcache_stats_write('flush', $bin, array('' => $rc));
}
return $rc;
}
/**
* Retrieves statistics recorded during memcache operations.
*
* @param string $stats_bin
* The bin to retrieve statistics for.
* @param string $stats_type
* The type of statistics to retrieve when using the Memcache extension.
* @param bool $aggregate
* Whether to aggregate statistics.
*/
function dmemcache_stats($stats_bin = 'cache', $stats_type = 'default', $aggregate = FALSE) {
$memcache_bins = variable_get('memcache_bins', array('cache' => 'default'));
// The stats_type can be over-loaded with an integer slab id, if doing a
// cachedump. We know we're doing a cachedump if $slab is non-zero.
$slab = (int) $stats_type;
$stats = array();
foreach ($memcache_bins as $bin => $target) {
if ($stats_bin == $bin) {
if ($mc = dmemcache_object($bin)) {
if ($mc instanceof Memcached) {
$stats[$bin] = $mc->getStats();
}
// The PHP Memcache extension 3.x version throws an error if the stats
// type is NULL or not in {reset, malloc, slabs, cachedump, items,
// sizes}. If $stats_type is 'default', then no parameter should be
// passed to the Memcache memcache_get_extended_stats() function.
elseif ($mc instanceof Memcache) {
if ($stats_type == 'default' || $stats_type == '') {
$stats[$bin] = $mc->getExtendedStats();
}
// If $slab isn't zero, then we are dumping the contents of a
// specific cache slab.
elseif (!empty($slab)) {
$stats[$bin] = $mc->getStats('cachedump', $slab);
}
else {
$stats[$bin] = $mc->getExtendedStats($stats_type);
}
}
}
}
}
// Optionally calculate a sum-total for all servers in the current bin.
if ($aggregate) {
// Some variables don't logically aggregate.
$no_aggregate = array(
'pid',
'time',
'version',
'pointer_size',
'accepting_conns',
'listen_disabled_num',
);
foreach ($stats as $bin => $servers) {
if (is_array($servers)) {
foreach ($servers as $server) {
if (is_array($server)) {
foreach ($server as $key => $value) {
if (!in_array($key, $no_aggregate)) {
if (isset($stats[$bin]['total'][$key])) {
$stats[$bin]['total'][$key] += $value;
}
else {
$stats[$bin]['total'][$key] = $value;
}
}
}
}
}
}
}
}
return $stats;
}
/**
* Determine which memcache extension to use: memcache or memcached.
*
* By default prefer the 'Memcache' PHP extension, though the default can be
* overridden by setting memcache_extension in settings.php.
*/
function dmemcache_extension() {
static $extension = NULL;
if ($extension === NULL) {
// If an extension is specified in settings.php, use that when available.
$preferred = variable_get('memcache_extension', NULL);
if (isset($preferred) && class_exists($preferred)) {
$extension = ucfirst(strtolower($preferred));
}
// If no extension is set, default to Memcache.
elseif (class_exists('Memcache')) {
$extension = 'Memcache';
}
elseif (class_exists('Memcached')) {
$extension = 'Memcached';
}
else {
$extension = FALSE;
}
}
return $extension;
}
/**
* Return a new memcache instance.
*/
function dmemcache_instance() {
static $error = FALSE;
$extension = dmemcache_extension();
if ($extension == 'Memcache') {
return new Memcache();
}
elseif ($extension == 'Memcached') {
$memcache = new Memcached();
$default_opts = array(
Memcached::OPT_COMPRESSION => FALSE,
Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
);
foreach ($default_opts as $key => $value) {
$memcache->setOption($key, $value);
}
// See README.txt for setting custom Memcache options when using the
// memcached PECL extension.
$memconf = variable_get('memcache_options', array());
foreach ($memconf as $key => $value) {
$memcache->setOption($key, $value);
}
return $memcache;
}
else {
if (!$error) {
register_shutdown_function('watchdog', 'memcache', 'You must enable the PHP memcache (recommended) or memcached extension to use memcache.inc.', array(), WATCHDOG_ERROR);
$error = TRUE;
}
}
return FALSE;
}
/**
* Initiate a connection to memcache.
*
* @param object $memcache
* A memcache instance obtained through dmemcache_instance.
* @param string $server
* A server string of the format "localhost:11211" or
* "unix:///path/to/socket".
* @param bool $connection
* TRUE or FALSE, whether the $memcache instance already has at least one
* open connection.
*
* @return bool
* TRUE or FALSE if connection was successful.
*/
function dmemcache_connect($memcache, $server, $connection) {
static $memcache_persistent = NULL;
$extension = dmemcache_extension();
list($host, $port) = explode(':', $server);
if ($extension == 'Memcache') {
// Allow persistent connection via Memcache extension -- note that this
// module currently doesn't support persistent connections with the
// Memcached extension. See http://drupal.org/node/822316#comment-4427676
// for details.
if (!isset($memcache_persistent)) {
$memcache_persistent = variable_get('memcache_persistent', FALSE);
}
// Support unix sockets of the format 'unix:///path/to/socket'.
if ($host == 'unix') {
// Use full protocol and path as expected by Memcache extension.
$host = $server;
$port = 0;
}
// When using the PECL memcache extension, we must use ->(p)connect
// for the first connection.
if (!$connection) {
$track_errors = ini_set('track_errors', '1');
$php_errormsg = '';
// The Memcache extension requires us to use (p)connect for the first
// server we connect to.
if ($memcache_persistent) {
$rc = @$memcache->pconnect($host, $port);
}
else {
$rc = @$memcache->connect($host, $port);
}
if (!empty($php_errormsg)) {
register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_object: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
$php_errormsg = '';
}
ini_set('track_errors', $track_errors);
}
else {
$rc = $memcache->addServer($host, $port, $memcache_persistent);
}
}
elseif ($extension == 'Memcached') {
// Support unix sockets of the format 'unix:///path/to/socket'.
if ($host == 'unix') {
// Strip 'unix://' as expected by Memcached extension.
$host = substr($server, 7);
$port = 0;
}
$rc = $memcache->addServer($host, $port);
}
else {
$rc = FALSE;
}
return $rc;
}
/**
* Close the connection to the memcache instance.
*/
function dmemcache_close($memcache) {
$extension = dmemcache_extension();
if ($extension == 'Memcache' && $memcache instanceof Memcache) {
$rc = @$memcache->close;
}
elseif ($extension == 'Memcached' && $memcache instanceof Memcached) {
$rc = @$memcache->quit;
}
else {
$rc = FALSE;
}
return $rc;
}
/**
* Return a Memcache object for the specified bin.
*
* Note that there is nothing preventing developers from calling this function
* directly to get the Memcache object. Do this if you need functionality not
* provided by this API or if you need to use legacy code. Otherwise, use the
* dmemcache (get, set, delete, flush) API functions provided here.
*
* @param string $bin
* The bin which is to be used.
* @param bool $flush
* Defaults to FALSE. Rebuild the bin/server/cache mapping.
*
* @return mixed
* A Memcache object, or FALSE on failure.
*/
function dmemcache_object($bin = NULL, $flush = FALSE) {
static $memcache_cache = array();
static $memcache_servers = array();
static $memcache_bins = array();
static $failed_connections = array();
if ($flush) {
foreach ($memcache_cache as $cluster) {
memcache_close($cluster);
}
$memcache_cache = array();
}
$extension = dmemcache_extension();
if (empty($memcache_cache) || empty($memcache_cache[$bin])) {
if (empty($memcache_servers)) {
// Load the variables from settings.php if set.
$memcache_servers = variable_get('memcache_servers', array('127.0.0.1:11211' => 'default'));
$memcache_bins = variable_get('memcache_bins', array('cache' => 'default'));
}
// If not manually set, default this cluster to 'default'.
$cluster = empty($memcache_bins[$bin]) ? 'default' : $memcache_bins[$bin];
// If not manually set, map this bin to 'cache' which maps to the 'default'
// cluster.
if (empty($memcache_bins[$bin]) && !empty($memcache_cache['cache'])) {
$memcache_cache[$bin] = &$memcache_cache['cache'];
}
else {
// Create a new memcache object for each cluster.
$memcache = dmemcache_instance();
// Track whether or not we've opened any memcache connections.
$connection = FALSE;
// Link all the servers to this cluster.
foreach ($memcache_servers as $server => $c) {
if ($c == $cluster && !isset($failed_connections[$server])) {
$rc = dmemcache_connect($memcache, $server, $connection);
if ($rc !== FALSE) {
// We've made at least one successful connection.
$connection = TRUE;
}
else {
// Memcache connection failure. We can't log to watchdog directly
// because we're in an early Drupal bootstrap phase where watchdog
// is non-functional. Instead, register a shutdown handler so it
// gets recorded at the end of the page load.
register_shutdown_function('watchdog', 'memcache', 'Failed to connect to memcache server: !server', array('!server' => $server), WATCHDOG_ERROR);
$failed_connections[$server] = FALSE;
}
}
}
if ($connection) {
// Map the current bin with the new Memcache object.
$memcache_cache[$bin] = $memcache;
// Now that all the servers have been mapped to this cluster, look for
// other bins that belong to the cluster and map them too.
foreach ($memcache_bins as $b => $c) {
if ($c == $cluster && $b != $bin) {
// Map this bin and cluster by reference.
$memcache_cache[$b] = &$memcache_cache[$bin];
}
}
}
}
}
return empty($memcache_cache[$bin]) ? FALSE : $memcache_cache[$bin];
}
/**
* Prefixes a key and ensures it is url safe.
*
* @param string $key
* The key to prefix and encode.
* @param string $bin
* The cache bin which the key applies to.
*
* @return string
* The prefixed and encoded key.
*/
function dmemcache_key($key, $bin = 'cache') {
$prefix = '';
if ($prefix = variable_get('memcache_key_prefix', '')) {
$prefix .= '-';
}
// When simpletest is running, emulate the simpletest database prefix here
// to avoid the child site setting cache entries in the parent site.
if (isset($GLOBALS['drupal_test_info']['test_run_id'])) {
$prefix .= $GLOBALS['drupal_test_info']['test_run_id'];
}
$full_key = urlencode($prefix . $bin . '-' . $key);
// Memcache truncates keys longer than 250 characters[*]. This could lead to
// cache collisions, so we hash keys that are longer than this while still
// retaining as much of the key bin and name as possible to aid in debugging.
// The hashing algorithm used is configurable, with sha1 selected by default
// as it performs quickly with minimal collisions. You can enforce shorter
// keys by setting memcache_key_max_length in settings.php.
// [*]https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L47
$maxlen = variable_get('memcache_key_max_length', 250);
if (strlen($full_key) > $maxlen) {
$full_key = urlencode($prefix . $bin) . '-' . hash(variable_get('memcache_key_hash_algorithm', 'sha1'), $key);
$full_key .= '-' . substr(urlencode($key), 0, ($maxlen - 1) - strlen($full_key) - 1);
}
return $full_key;
}
/**
* Collect statistics if enabled.
*
* Optimized function to determine whether or not we should be collecting
* statistics. Also starts a timer to track how long individual memcache
* operations take.
*
* @return bool
* TRUE or FALSE if statistics should be collected.
*/
function dmemcache_stats_init() {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast = &drupal_static(__FUNCTION__, array('variable_checked' => NULL, 'user_access_checked' => NULL));
}
$variable_checked = &$drupal_static_fast['variable_checked'];
$user_access_checked = &$drupal_static_fast['user_access_checked'];
// Confirm DRUPAL_BOOTSTRAP_VARIABLES has been reached. We don't use
// drupal_get_bootstrap_phase() as it's buggy. We can use variable_get() here
// because _drupal_bootstrap_variables() includes module.inc immediately
// after it calls variable_initialize().
if (!isset($variable_checked) && function_exists('module_list')) {
$variable_checked = variable_get('show_memcache_statistics', FALSE);
}
// If statistics are enabled we need to check user access.
if (!empty($variable_checked) && !isset($user_access_checked) && !empty($GLOBALS['user']) && function_exists('user_access')) {
// Statistics are enabled and the $user object has been populated, so check
// that the user has access to view them.
$user_access_checked = user_access('access memcache statistics');
}
// Return whether or not statistics are enabled and the user can access them.
if ((!isset($variable_checked) || $variable_checked) && (!isset($user_access_checked) || $user_access_checked)) {
timer_start('dmemcache');
return TRUE;
}
else {
return FALSE;
}
}
/**
* Save memcache statistics to be displayed at end of page generation.
*
* @param string $action
* The action being performed (get, set, etc...).
* @param string $bin
* The memcache bin the action is being performed in.
* @param array $keys
* Keyed array in the form (string)$cid => (bool)$success. The keys the
* action is being performed on, and whether or not it was a success.
*/
function dmemcache_stats_write($action, $bin, $keys) {
global $_dmemcache_stats;
// Determine how much time elapsed to execute this action.
$time = timer_read('dmemcache');
// Build the 'all' and 'ops' arrays displayed by memcache_admin.module.
foreach ($keys as $key => $success) {
$_dmemcache_stats['all'][] = array(
number_format($time, 2),
$action,
$bin,
$key,
$success ? 'hit' : 'miss',
);
if (!isset($_dmemcache_stats['ops'][$action])) {
$_dmemcache_stats['ops'][$action] = array($action, 0, 0, 0);
}
$_dmemcache_stats['ops'][$action][1] += $time;
if ($success) {
$_dmemcache_stats['ops'][$action][2]++;
}
else {
$_dmemcache_stats['ops'][$action][3]++;
}
}
// Reset the dmemcache timer for timing the next memcache operation.
}