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); if (empty($rc)) { // If there was a MEMCACHED_E2BIG error, split the value into pieces // and cache them individually. if ($mc->getResultCode() == MEMCACHED_E2BIG) { $rc = _dmemcache_set_pieces($key, $value, $exp, $bin, $mc); } } } else { // The PECL Memcache library throws an E_NOTICE level error, which // $php_errormsg doesn't catch, so we need to log it ourselves. // Catch it with our own error handler. drupal_static_reset('_dmemcache_error_handler'); set_error_handler('_dmemcache_error_handler'); $rc = $mc->set($full_key, $value, MEMCACHE_COMPRESSED, $exp); // Restore the Drupal error handler. restore_error_handler(); if (empty($rc)) { // If the object was too big, split the value into pieces and cache // them individually. $dmemcache_errormsg = &drupal_static('_dmemcache_error_handler'); if (!empty($dmemcache_errormsg) && (strpos($dmemcache_errormsg, 'SERVER_ERROR object too large for cache') !== FALSE || strpos($dmemcache_errormsg, 'SERVER_ERROR out of memory storing object') !== FALSE)) { $rc = _dmemcache_set_pieces($key, $value, $exp, $bin, $mc); } } } } if ($collect_stats) { dmemcache_stats_write('set', $bin, array($full_key => $rc)); } _dmemcache_write_debug('set', $bin, $full_key, $rc); return $rc; } /** * A temporary error handler which keeps track of the most recent error. */ function _dmemcache_error_handler($errno, $errstr) { $dmemcache_errormsg = &drupal_static(__FUNCTION__); $dmemcache_errormsg = $errstr; return TRUE; } /** * Split a large item into pieces and place them 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 * (optional) 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 * (optional) 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 * (optional) The memcache object. Normally this value is * determined automatically based on the bin the object is being stored to. * * @return bool */ function _dmemcache_set_pieces($key, $value, $exp = 0, $bin = 'cache', $mc = NULL) { static $recursion = 0; if (!empty($value->multi_part_data) || !empty($value->multi_part_pieces)) { // Prevent an infinite loop. return FALSE; } // Recursion happens when __dmemcache_piece_cache outgrows the largest // memcache slice (1 MiB by default) -- prevent an infinite loop and later // generate a watchdog error. if ($recursion) { return FALSE; } $recursion++; $full_key = dmemcache_key($key); // Cache the name of this key so if it is deleted later we know to also // delete the cache pieces. if (!dmemcache_piece_cache_set($full_key, $exp)) { // We're caching a LOT of large items. Our piece_cache has exceeded the // maximum memcache object size (default of 1 MiB). $piece_cache = &drupal_static('dmemcache_piece_cache', array()); register_shutdown_function('watchdog', 'memcache', 'Too many over-sized cache items (!count) has caused the dmemcache_piece_cache to exceed the maximum memcache object size (default of 1 MiB). Now relying on memcache auto-expiration to eventually clean up over-sized cache pieces upon deletion.', array('!count' => count($piece_cache)), WATCHDOG_ERROR); } if (variable_get('memcache_log_data_pieces', 2)) { timer_start('memcache_split_data'); } // We need to split the item into pieces, so convert it into a string. if (is_string($value)) { $data = $value; $serialized = FALSE; } else { $serialize_function = dmemcache_serialize(); $data = $serialize_function($value); $serialized = TRUE; } // Account for any metadata stored alongside the data. $max_len = variable_get('memcache_data_max_length', 1048576) - (512 + strlen($full_key)); $pieces = str_split($data, $max_len); $piece_count = count($pieces); // Create a placeholder item containing data about the pieces. $cache = new stdClass; // $key gets run through dmemcache_key() later inside dmemcache_set(). $cache->cid = $key; $cache->created = REQUEST_TIME; $cache->expire = $exp; $cache->data = new stdClass; $cache->data->serialized = $serialized; $cache->data->piece_count = $piece_count; $cache->multi_part_data = TRUE; $result = dmemcache_set($cache->cid, $cache, $exp, $bin, $mc); // Create a cache item for each piece of data. foreach ($pieces as $id => $piece) { $cache = new stdClass; $cache->cid = _dmemcache_key_piece($key, $id); $cache->created = REQUEST_TIME; $cache->expire = $exp; $cache->data = $piece; $cache->multi_part_piece = TRUE; $result &= dmemcache_set($cache->cid, $cache, $exp, $bin, $mc); } if (variable_get('memcache_log_data_pieces', 2) && $piece_count >= variable_get('memcache_log_data_pieces', 2)) { if (function_exists('format_size')) { $data_size = format_size(strlen($data)); } else { $data_size = number_format(strlen($data)) . ' byte'; } register_shutdown_function('watchdog', 'memcache', 'Spent !time ms splitting !bytes object into !pieces pieces, cid = !key', array('!time' => timer_read('memcache_split_data'), '!bytes' => $data_size, '!pieces' => $piece_count, '!key' => dmemcache_key($key, $bin)), WATCHDOG_WARNING); } $recursion--; // TRUE if all pieces were saved correctly. return $result; } /** * 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 * (optional) 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 * (optional) 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 * (optional) The memcache object. Normally this value is * determined automatically based on the bin the object is being stored to. * @param bool $flag * (optional) 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)); } _dmemcache_write_debug('add', $bin, $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); // This is a multi-part value. if (is_object($result) && !empty($result->multi_part_data)) { $result = _dmemcache_get_pieces($result->data, $result->cid, $bin, $mc); } 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)); } _dmemcache_write_debug('get', $bin, $full_key, (bool) $result); return $result; } /** * Retrieve a value from the cache. * * @param $item * The placeholder cache item from _dmemcache_set_pieces(). * @param $key * The key with which the item was stored. * @param string $bin * (optional) The bin in which the item was stored. * @param object $mc * (optional) The memcache object. Normally this value is * determined automatically based on the bin the object is being stored to. * * @return object|bool * The item which was originally saved or FALSE. */ function _dmemcache_get_pieces($item, $key, $bin = 'cache', $mc = NULL) { // Create a list of keys for the pieces of data. for ($id = 0; $id < $item->piece_count; $id++) { $keys[] = _dmemcache_key_piece($key, $id); } // Retrieve all the pieces of data. $pieces = dmemcache_get_multi($keys, $bin, $mc); if (count($pieces) != $item->piece_count) { // Some of the pieces don't exist, so our data cannot be reconstructed. return FALSE; } // Append all of the pieces together. $data = ''; foreach ($pieces as $piece) { $data .= $piece->data; } unset($pieces); // If necessary unserialize the item. if (empty($item->serialized)) { return $data; } else { $unserialize_function = dmemcache_unserialize(); return $unserialize_function($data); } } /** * Generates a key name for a multi-part data piece based on the sequence ID. * * @param int $id * The sequence ID of the data piece. * @param int $key * The original CID of the cache item. * * @return string */ function _dmemcache_key_piece($key, $id) { return dmemcache_key('_multi'. (string)$id . "-$key"); } /** * 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) { if (PHP_MAJOR_VERSION >= 7) { // See https://www.drupal.org/node/2728427 $results = $mc->getMulti($full_keys, Memcached::GET_PRESERVE_ORDER); } else { $cas_tokens = NULL; $results = $mc->getMulti($full_keys, $cas_tokens, Memcached::GET_PRESERVE_ORDER); } } 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); // Order is not guaranteed, map responses to order of requests. if (is_array($results)) { $keys = array_fill_keys($full_keys, NULL); $results = array_merge($keys, $results); } } } // If $results is FALSE, convert it to an empty array. if (!$results) { $results = array(); _dmemcache_write_debug('getMulti', $bin, implode(',', $full_keys), FALSE); } 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) { // This is a multi-part value. if (is_object($value) && !empty($value->multi_part_data)) { $value = _dmemcache_get_pieces($value->data, $value->cid, $bin, $mc); } $cid_results[$cid_lookup[$key]] = $value; _dmemcache_write_debug('getMulti', $bin, $key, TRUE); } 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_keys = dmemcache_key($key, $bin, TRUE); $rc = FALSE; if ($mc || ($mc = dmemcache_object($bin))) { foreach ($full_keys as $fk) { $rc = $mc->delete($fk, 0); if ($rc) { // If the delete succeeded, we now check to see if this item has // multiple pieces also needing to be cleaned up. If the delete failed, // we assume these keys have already expired or been deleted (memcache // will auto-expire eventually if we're wrong). if ($piece_cache = dmemcache_piece_cache_get($fk)) { // First, remove from the piece_cache so we don't try and delete it // again in another thread, then delete the actual cache data pieces. dmemcache_piece_cache_set($fk, NULL); $next_id = 0; do { if ($inner_rc) { _dmemcache_write_debug("delete", $bin, $full_key, $inner_rc); } // Generate the cid of the next data piece. $piece_key = _dmemcache_key_piece($key, $next_id); $full_key = dmemcache_key($piece_key, $bin); $next_id++; // Keep deleting pieces until the operation fails. We accept that // this could lead to orphaned pieces as memcache will auto-expire // them eventually. } while ($inner_rc = $mc->delete($full_key, 0)); _dmemcache_write_debug("delete", $bin, $full_key, $inner_rc); // Perform garbage collection for keys memcache has auto-expired. If // we don't do this, this variable could grow over enough time as a // slow memory leak. // @todo: Consider moving this to hook_cron() and requiring people to // enable the memcache module. timer_start('memcache_gc_piece_cache'); $gc_counter = 0; $piece_cache = &drupal_static('dmemcache_piece_cache', array()); foreach ($piece_cache as $cid => $expires) { if (REQUEST_TIME > $expires) { $gc_counter++; dmemcache_piece_cache_set($cid, NULL); } } if ($gc_counter) { register_shutdown_function('watchdog', 'memcache', 'Spent !time ms in garbage collection cleaning !count stale keys from the dmemcache_piece_cache.', array('!time' => timer_read('memcache_gc_piece_cache'), '!count' => $gc_counter), WATCHDOG_WARNING); } } } if ($collect_stats) { dmemcache_stats_write('delete', $bin, array($fk => $rc)); } _dmemcache_write_debug('delete', $bin, $fk, $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 = $mc->flush(); } if ($collect_stats) { dmemcache_stats_write('flush', $bin, array('' => $rc)); } _dmemcache_write_debug('flush', $bin, $full_key, $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; } /** * Determine which serialize extension to use: serialize (none), igbinary, * or msgpack. * * By default we prefer the igbinary extension, then the msgpack extension, * then the core serialize functions. This can be overridden in settings.php. */ function dmemcache_serialize_extension() { static $extension = NULL; if ($extension === NULL) { $preferred = strtolower(variable_get('memcache_serialize', NULL)); // Fastpath if we're forcing php's own serialize function. if ($preferred == 'serialize') { $extension = $preferred; } // Otherwise, find an available extension favoring configuration. else { $igbinary_available = extension_loaded('igbinary'); $msgpack_available = extension_loaded('msgpack'); if ($preferred == 'igbinary' && $igbinary_available) { $extension = $preferred; } elseif ($preferred == 'msgpack' && $msgpack_available) { $extension = $preferred; } else { // No (valid) serialize extension specified, try igbinary. if ($igbinary_available) { $extension = 'igbinary'; } // Next try msgpack. else if ($msgpack_available) { $extension = 'msgpack'; } // Finally fall back to core serialize. else { $extension = 'serialize'; } } } } return $extension; } /** * Return proper serialize function. */ function dmemcache_serialize() { switch (dmemcache_serialize_extension()) { case 'igbinary': return 'igbinary_serialize'; case 'msgpack': return 'msgpack_pack'; default: return 'serialize'; } } /** * Return proper unserialize function. */ function dmemcache_unserialize() { switch (dmemcache_serialize_extension()) { case 'igbinary': return 'igbinary_unserialize'; case 'msgpack': return 'msgpack_unpack'; default: return 'unserialize'; } } /** * Return a new memcache instance. */ function dmemcache_instance($bin = 'cache') { static $error = FALSE; $extension = dmemcache_extension(); if ($extension == 'Memcache') { return new Memcache(); } elseif ($extension == 'Memcached') { if (variable_get('memcache_persistent', TRUE)) { $memcache = new Memcached($bin); } else { $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); } if (($sasl_username = variable_get('memcache_sasl_username', '')) && ($sasl_password = variable_get('memcache_sasl_password', ''))) { $memcache->setSaslAuthData($sasl_username, $sasl_password); } 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(':', trim($server)); if (empty($host)) { register_shutdown_function('watchdog', 'memcache', 'You have specified an invalid address of "!server" in settings.php. Please review README.txt for proper configuration.', array('!server' => $server, '!ip' => t('127.0.0.1:11211'), '!host' => t('localhost:11211'), '!socket' => t('unix:///path/to/socket')), WATCHDOG_WARNING); } if (!isset($memcache_persistent)) { $memcache_persistent = variable_get('memcache_persistent', TRUE); } if ($extension == 'Memcache') { // 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; } else if (!isset($port)) { register_shutdown_function('watchdog', 'memcache', 'You have specified an invalid address of "!server" in settings.php which does not include a port. Please review README.txt for proper configuration. You must specify both a server address and port such as "!ip" or "!host", or a unix socket such as "!socket".', array('!server' => $server, '!ip' => t('127.0.0.1:11211'), '!host' => t('localhost:11211'), '!socket' => t('unix:///path/to/socket')), WATCHDOG_WARNING); } // 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_connect while connecting to !host:!port: !msg', array('!host' => $host, '!port' => $port, '!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; } else if (!isset($port)) { register_shutdown_function('watchdog', 'memcache', 'You have specified an invalid address of "!server" in settings.php which does not include a port. Please review README.txt for proper configuration. You must specify both a server address and port such as "!ip" or "!host", or a unix socket such as "!socket".', array('!server' => $server, '!ip' => t('127.0.0.1:11211'), '!host' => t('localhost:11211'), '!socket' => t('unix:///path/to/socket')), WATCHDOG_WARNING); } if ($memcache_persistent) { $servers = $memcache->getServerList(); $match = FALSE; foreach ($servers as $s) { if ($s['host'] == $host && $s['port'] == $port) { $match = TRUE; break; } } if (!$match) { $rc = $memcache->addServer($host, $port); } else { $rc = TRUE; } } else { $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) { if (!empty($memcache_cache)) { foreach ($memcache_cache as $cluster) { if ($extension == 'Memcache' && $cluster instanceof Memcache) { $rc = @$cluster->close; } elseif ($extension == 'Memcached' && $cluster instanceof Memcached) { $rc = @$cluster->quit; } $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($bin); // 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. * @param string $multiple * If TRUE will return all possible prefix variations. * * @return string or array * The prefixed and encoded key(s). */ function dmemcache_key($key, $bin = 'cache', $multiple = FALSE) { $prefix = ''; if ($prefixes = variable_get('memcache_key_prefix', '')) { if (is_array($prefixes)) { // If no custom prefix defined for bin, use 'default'. if (empty($prefixes[$bin])) { $bin = 'default'; } if (!empty($prefixes[$bin])) { // There can be multiple prefixes specified for each bin. if (is_array($prefixes[$bin])) { // Optionally return key with all prefixes. if ($multiple) { $prefix = array(); foreach ($prefixes[$bin] as $pre) { $prefix[] = $pre . '-'; } } // Otherwise just return a single prefixed key. else { $prefix = $prefixes[$bin][0] . '-'; } } } } else { $prefix = $prefixes . '-'; } } if (!is_array($prefix)) { $prefix = array($prefix); } $full_keys = array(); foreach ($prefix as $p) { // 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'])) { $p .= $GLOBALS['drupal_test_info']['test_run_id']; } $full_keys[] = urlencode($p . $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); foreach ($full_keys as $k => $full_key) { if (strlen($full_key) > $maxlen) { $full_keys[$k] = urlencode($prefix[$k] . $bin) . '-' . hash(variable_get('memcache_key_hash_algorithm', 'sha1'), $key); $full_keys[$k] .= '-' . substr(urlencode($key), 0, ($maxlen - 1) - strlen($full_keys[$k]) - 1); } } if ($multiple) { // An array of prefixed keys. return $full_keys; } else { // A single prefixed key. return array_shift($full_keys); } } /** * Track active keys with multi-piece values, necessary for efficient cleanup. * * We can't use variable_get/set for tracking this information because if the * variables array grows >1M and has to be split into pieces we'd get stuck in * an infinite loop. Storing this information in memcache means it can be lost, * but in that case the pieces will still eventually be auto-expired by * memcache. * * @param string $cid * The cid of the root multi-piece value. * @param integer $exp * Timestamp when the cached item expires. If NULL, the $cid will be deleted. * * @return bool * TRUE on succes, FALSE otherwise. */ function dmemcache_piece_cache_set($cid, $exp = NULL) { // Always refresh cached copy to minimize multi-thread race window. $piece_cache = &drupal_static('dmemcache_piece_cache', array()); $piece_cache = dmemcache_get('__dmemcache_piece_cache'); if (isset($exp)) { if ($exp <= 0) { // If no expiration time is set, defaults to 30 days. $exp = REQUEST_TIME + 2592000; } $piece_cache[$cid] = $exp; } else { unset($piece_cache[$cid]); } return dmemcache_set('__dmemcache_piece_cache', $piece_cache); } /** * Determine if a key has multi-piece values. * * * @param string $cid * The cid to check for multi-piece values. * * @return integer * Expiration time if key has multi-piece values, otherwise FALSE. */ function dmemcache_piece_cache_get($name) { static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['piece_cache'] = &drupal_static('dmemcache_piece_cache', FALSE); } $piece_cache = &$drupal_static_fast['piece_cache']; if (!is_array($piece_cache)) { $piece_cache = dmemcache_get('__dmemcache_piece_cache'); // On a website with no over-sized cache pieces, initialize the variable so // we never load it more than once per page versus once per DELETE. if (!is_array($piece_cache)) { dmemcache_set('__dmemcache_piece_cache', array()); } } if (isset($piece_cache[$name])) { // Return the expiration time of the multi-piece cache item. return $piece_cache[$name]; } // Item doesn't have multiple pieces. return FALSE; } /** * 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. } /** * Write to a debug log when enabled. * * @param string $action * The action being performed (get, set, etc...). * @param string $bin * The memcache bin the action is being performed in. * @param array $rc * The return code of the action performed. */ function _dmemcache_write_debug($action, $bin, $key, $rc) { static $memcache_debug_log = NULL; static $memcache_debug_verbose = NULL; if ($memcache_debug_log === NULL) { $memcache_debug_log = variable_get('memcache_debug_log', FALSE); $memcache_debug_verbose = variable_get('memcache_debug_verbose', FALSE); } if (($action == 'get' || $action == 'getMulti') && !$memcache_debug_verbose) { return; } else if ($memcache_debug_log) { $debug_log = strtr(variable_get('memcache_debug_log_format', "!timestamp|!action|!bin|!cid|!rc\n"), array('!timestamp' => date(variable_get('memcache_debug_time_format', 'U')), '!action' => $action, '!bin' => $bin, '!cid' => $key, '!rc' => (int) $rc)); if (file_put_contents($memcache_debug_log, $debug_log, FILE_APPEND) === FALSE) { // This can lead to a lot of watchdog entries... register_shutdown_function('watchdog', 'memcache', 'Unable to write to debug log at !debug_file: !debug_log.', array('!debug_file' => $memcache_debug_file, '!debug_log' => $debug_log), WATCHDOG_ERROR); } } }