You are here

class MemcacheBackend in Memcache API and Integration 8.2

Defines a Memcache cache backend.

Hierarchy

Expanded class hierarchy of MemcacheBackend

File

src/MemcacheBackend.php, line 17

Namespace

Drupal\memcache
View source
class MemcacheBackend implements CacheBackendInterface {
  use LoggerChannelTrait;

  /**
   * The maximum size of an individual cache chunk.
   *
   * Memcached is about balance. With this area of functionality, we need to
   * minimize the number of split items while also considering wasted memory.
   * In Memcached, all slab "pages" contain 1MB of data, by default.  Therefore,
   * when we split items, we want to do to in a manner that comes close to
   * filling a slab page with as little remaining memory as possible, while
   * taking item overhead into consideration.
   *
   * Our tests concluded that Memached slab 39 is a perfect slab to target.
   * Slab 39 contains items roughly between 385-512KB in size.  We are targeting
   * a chunk size of 493568 bytes (482kb) - which will give us enough storage
   * for two split items, leaving as little overhead as possible.
   *
   * Note that the overhead not only includes metadata about each item, but
   * also allows compression "backfiring" (under some circumstances, compression
   * actually enlarges some data objects instead of shrinking them).   */
  const MAX_CHUNK_SIZE = 470000;

  /**
   * The cache bin to use.
   *
   * @var string
   */
  protected $bin;

  /**
   * The (micro)time the bin was last deleted.
   *
   * @var float
   */
  protected $lastBinDeletionTime;

  /**
   * The memcache wrapper object.
   *
   * @var \Drupal\memcache\DrupalMemcacheInterface
   */
  protected $memcache;

  /**
   * The cache tags checksum provider.
   *
   * @var \Drupal\Core\Cache\CacheTagsChecksumInterface|\Drupal\Core\Cache\CacheTagsInvalidatorInterface
   */
  protected $checksumProvider;

  /**
   * The timestamp invalidation provider.
   *
   * @var \Drupal\memcache\Invalidator\TimestampInvalidatorInterface
   */
  protected $timestampInvalidator;

  /**
   * Constructs a MemcacheBackend object.
   *
   * @param string $bin
   *   The bin name.
   * @param \Drupal\memcache\DrupalMemcacheInterface $memcache
   *   The memcache object.
   * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
   *   The cache tags checksum service.
   * @param \Drupal\memcache\Invalidator\TimestampInvalidatorInterface $timestamp_invalidator
   *   The timestamp invalidation provider.
   */
  public function __construct($bin, DrupalMemcacheInterface $memcache, CacheTagsChecksumInterface $checksum_provider, TimestampInvalidatorInterface $timestamp_invalidator) {
    $this->bin = $bin;
    $this->memcache = $memcache;
    $this->checksumProvider = $checksum_provider;
    $this->timestampInvalidator = $timestamp_invalidator;
    $this
      ->ensureBinDeletionTimeIsSet();
  }

  /**
   * Check to see if debug is on. Wrap it in safety for early bootstraps.
   *
   * @returns bool
   */
  private function debug() : bool {
    try {
      $debug = \Drupal::service('memcache.settings')
        ->get('debug');
      if ($debug) {
        return $debug;
      }
      return false;
    } catch (ServiceNotFoundException $e) {
      return false;
    } catch (ContainerNotInitializedException $e) {
      return false;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function get($cid, $allow_invalid = FALSE) {
    $cids = [
      $cid,
    ];
    $cache = $this
      ->getMultiple($cids, $allow_invalid);
    return reset($cache);
  }

  /**
   * {@inheritdoc}
   */
  public function getMultiple(&$cids, $allow_invalid = FALSE) {
    $cache = $this->memcache
      ->getMulti($cids);
    $fetched = [];
    foreach ($cache as $result) {
      if (!$this
        ->timeIsGreaterThanBinDeletionTime($result->created)) {
        continue;
      }
      if ($this
        ->valid($result->cid, $result) || $allow_invalid) {

        // If the item is multipart, rebuild the original cache data by fetching
        // children and combining them back into a single item.
        if ($result->data instanceof MultipartItem) {
          $childCIDs = $result->data
            ->getCids();
          $dataParts = $this->memcache
            ->getMulti($childCIDs);
          if (count($dataParts) !== count($childCIDs)) {

            // We're missing a chunk of the original entry. It is not valid.
            continue;
          }
          $result->data = $this
            ->combineItems($dataParts);
        }

        // Add it to the fetched items to diff later.
        $fetched[$result->cid] = $result;
      }
    }

    // Remove items from the referenced $cids array that we are returning,
    // per comment in Drupal\Core\Cache\CacheBackendInterface::getMultiple().
    $cids = array_diff($cids, array_keys($fetched));
    return $fetched;
  }

  /**
   * Determines if the cache item is valid.
   *
   * This also alters the valid property of the cache item itself.
   *
   * @param string $cid
   *   The cache ID.
   * @param \stdClass $cache
   *   The cache item.
   *
   * @return bool
   *   TRUE if valid, FALSE otherwise.
   */
  protected function valid($cid, \stdClass $cache) {
    $cache->valid = TRUE;

    // Items that have expired are invalid.
    if ($cache->expire != CacheBackendInterface::CACHE_PERMANENT && $cache->expire <= REQUEST_TIME) {
      $cache->valid = FALSE;
    }

    // Check if invalidateTags() has been called with any of the items's tags.
    if (!$this->checksumProvider
      ->isValid($cache->checksum, $cache->tags)) {
      $cache->valid = FALSE;
    }
    return $cache->valid;
  }

  /**
   * {@inheritdoc}
   */
  public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = []) {
    assert(Inspector::assertAllStrings($tags));
    $tags[] = "memcache:{$this->bin}";
    $tags = array_unique($tags);

    // Sort the cache tags so that they are stored consistently.
    sort($tags);

    // Create new cache object.
    $cache = new \stdClass();
    $cache->cid = $cid;
    $cache->data = $data;
    $cache->created = round(microtime(TRUE), 3);
    $cache->expire = $expire;
    $cache->tags = $tags;
    $cache->checksum = $this->checksumProvider
      ->getCurrentChecksum($tags);

    // Cache all items permanently. We handle expiration in our own logic.
    if ($this->memcache
      ->set($cid, $cache)) {
      return TRUE;
    }

    // Assume that the item is too large.  We need to split it into multiple
    // chunks with a parent entry referencing all the chunks.
    $childKeys = [];
    foreach ($this
      ->splitItem($cache) as $part) {

      // If a single chunk fails to be set, stop trying - we can't reconstitute
      // a value with a missing chunk.
      if (!$this->memcache
        ->set($part->cid, $part)) {
        return FALSE;
      }
      $childKeys[] = $part->cid;
    }

    // Create and write the parent entry referencing all chunks.
    $cache->data = new MultipartItem($childKeys);
    return $this->memcache
      ->set($cid, $cache);
  }

  /**
   * Given a single cache item, split it into multiple child items.
   *
   * @param \stdClass $item
   *   The original cache item, before the split.
   *
   * @return \stdClass[]
   *   An array of child items.
   */
  private function splitItem(\stdClass $item) {
    $data = serialize($item->data);
    $pieces = str_split($data, static::MAX_CHUNK_SIZE);

    // Add a unique identifier each time this function is invoked.  This
    // prevents a race condition where two sets on the same multipart item can
    // clobber each other's children.  With this seed, each time a multipart
    // entry is created, they get a different CID.  The parent (multipart) entry
    // does not inherit this unique identifier, so it is still addressable using
    // the CID it was initially given.
    $seed = Crypt::randomBytesBase64();
    $children = [];
    foreach ($pieces as $i => $chunk) {

      // Child items do not need tags or expire, since that data is carried by
      // the parent.
      $chunkItem = new \stdClass();

      // @TODO: mention why we added split and picked this order...
      $chunkItem->cid = sprintf('split.%d.%s.%s', $i, $item->cid, $seed);
      $chunkItem->data = $chunk;
      $chunkItem->created = $item->created;
      $children[] = $chunkItem;
    }
    if ($this
      ->debug()) {
      $this
        ->getLogger('memcache')
        ->debug('Split item @cid into @num pieces', [
        '@cid' => $item->cid,
        '@num' => $i + 1,
      ]);
    }
    return $children;
  }

  /**
   * Given an array of child cache items, recombine into a single value.
   *
   * @param \stdClass[] $items
   *   An array of child cache items.
   *
   * @return mixed
   *   The combined an unserialized value that was originally stored.
   */
  private function combineItems(array $items) {
    $data = array_reduce($items, function ($collected, $item) {
      return $collected . $item->data;
    }, '');
    return unserialize($data);
  }

  /**
   * {@inheritdoc}
   */
  public function setMultiple(array $items) {
    foreach ($items as $cid => $item) {
      $item += [
        'expire' => CacheBackendInterface::CACHE_PERMANENT,
        'tags' => [],
      ];
      $this
        ->set($cid, $item['data'], $item['expire'], $item['tags']);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function delete($cid) {
    $this->memcache
      ->delete($cid);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteMultiple(array $cids) {
    foreach ($cids as $cid) {
      $this->memcache
        ->delete($cid);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAll() {
    if ($this
      ->debug()) {
      $this
        ->getLogger('memcache')
        ->debug('Called deleteAll() on bin @bin', [
        '@bin' => $this->bin,
      ]);
    }
    $this->lastBinDeletionTime = $this->timestampInvalidator
      ->invalidateTimestamp($this->bin);
  }

  /**
   * {@inheritdoc}
   */
  public function invalidate($cid) {
    $this
      ->invalidateMultiple([
      $cid,
    ]);
  }

  /**
   * Marks cache items as invalid.
   *
   * Invalid items may be returned in later calls to get(), if the
   * $allow_invalid argument is TRUE.
   *
   * @param array $cids
   *   An array of cache IDs to invalidate.
   *
   * @see Drupal\Core\Cache\CacheBackendInterface::deleteMultiple()
   * @see Drupal\Core\Cache\CacheBackendInterface::invalidate()
   * @see Drupal\Core\Cache\CacheBackendInterface::invalidateTags()
   * @see Drupal\Core\Cache\CacheBackendInterface::invalidateAll()
   */
  public function invalidateMultiple(array $cids) {
    foreach ($cids as $cid) {
      if ($item = $this
        ->get($cid)) {
        $item->expire = REQUEST_TIME - 1;
        $this->memcache
          ->set($cid, $item);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function invalidateAll() {
    if ($this
      ->debug()) {
      $this
        ->getLogger('memcache')
        ->debug('Called invalidateAll() on bin @bin', [
        '@bin' => $this->bin,
      ]);
    }
    $this
      ->invalidateTags([
      "memcache:{$this->bin}",
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function invalidateTags(array $tags) {
    if ($this
      ->debug()) {
      $this
        ->getLogger('memcache')
        ->debug('Called invalidateTags() on tags @tags', [
        '@tags' => implode(',', $tags),
      ]);
    }
    $this->checksumProvider
      ->invalidateTags($tags);
  }

  /**
   * {@inheritdoc}
   */
  public function removeBin() {
    if ($this
      ->debug()) {
      $this
        ->getLogger('memcache')
        ->debug('Called removeBin() on bin @bin', [
        '@bin' => $this->bin,
      ]);
    }
    $this->lastBinDeletionTime = $this->timestampInvalidator
      ->invalidateTimestamp($this->bin);
  }

  /**
   * {@inheritdoc}
   */
  public function garbageCollection() {

    // Memcache will invalidate items; That items memory allocation is then
    // freed up and reused. So nothing needs to be deleted/cleaned up here.
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty() {

    // We do not know so err on the safe side? Not sure if we can know this?
    return TRUE;
  }

  /**
   * Determines if a (micro)time is greater than the last bin deletion time.
   *
   * @param float $item_microtime
   *   A given (micro)time.
   *
   * @internal
   *
   * @return bool
   *   TRUE if the (micro)time is greater than the last bin deletion time, FALSE
   *   otherwise.
   */
  protected function timeIsGreaterThanBinDeletionTime($item_microtime) {
    $last_bin_deletion = $this
      ->getBinLastDeletionTime();

    // If there is time, assume FALSE as there is no previous deletion time
    // to compare with.
    if (!$last_bin_deletion) {
      return FALSE;
    }
    return $item_microtime > $last_bin_deletion;
  }

  /**
   * Gets the last invalidation time for the bin.
   *
   * @internal
   *
   * @return float
   *   The last invalidation timestamp of the tag.
   */
  protected function getBinLastDeletionTime() {
    if (!isset($this->lastBinDeletionTime)) {
      $this->lastBinDeletionTime = $this->timestampInvalidator
        ->getLastInvalidationTimestamp($this->bin);
    }
    return $this->lastBinDeletionTime;
  }

  /**
   * Ensures a last bin deletion time has been set.
   *
   * @internal
   */
  protected function ensureBinDeletionTimeIsSet() {
    if (!$this
      ->getBinLastDeletionTime()) {
      $this->lastBinDeletionTime = $this->timestampInvalidator
        ->invalidateTimestamp($this->bin);
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CacheBackendInterface::CACHE_PERMANENT constant Indicates that the item should never be removed unless explicitly deleted.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MemcacheBackend::$bin protected property The cache bin to use.
MemcacheBackend::$checksumProvider protected property The cache tags checksum provider.
MemcacheBackend::$lastBinDeletionTime protected property The (micro)time the bin was last deleted.
MemcacheBackend::$memcache protected property The memcache wrapper object.
MemcacheBackend::$timestampInvalidator protected property The timestamp invalidation provider.
MemcacheBackend::combineItems private function Given an array of child cache items, recombine into a single value.
MemcacheBackend::debug private function Check to see if debug is on. Wrap it in safety for early bootstraps.
MemcacheBackend::delete public function Deletes an item from the cache. Overrides CacheBackendInterface::delete
MemcacheBackend::deleteAll public function Deletes all cache items in a bin. Overrides CacheBackendInterface::deleteAll
MemcacheBackend::deleteMultiple public function Deletes multiple items from the cache. Overrides CacheBackendInterface::deleteMultiple
MemcacheBackend::ensureBinDeletionTimeIsSet protected function Ensures a last bin deletion time has been set.
MemcacheBackend::garbageCollection public function Performs garbage collection on a cache bin. Overrides CacheBackendInterface::garbageCollection
MemcacheBackend::get public function Returns data from the persistent cache. Overrides CacheBackendInterface::get
MemcacheBackend::getBinLastDeletionTime protected function Gets the last invalidation time for the bin.
MemcacheBackend::getMultiple public function Returns data from the persistent cache when given an array of cache IDs. Overrides CacheBackendInterface::getMultiple
MemcacheBackend::invalidate public function Marks a cache item as invalid. Overrides CacheBackendInterface::invalidate
MemcacheBackend::invalidateAll public function Marks all cache items as invalid. Overrides CacheBackendInterface::invalidateAll
MemcacheBackend::invalidateMultiple public function Marks cache items as invalid. Overrides CacheBackendInterface::invalidateMultiple
MemcacheBackend::invalidateTags public function
MemcacheBackend::isEmpty public function
MemcacheBackend::MAX_CHUNK_SIZE constant The maximum size of an individual cache chunk.
MemcacheBackend::removeBin public function Remove a cache bin. Overrides CacheBackendInterface::removeBin
MemcacheBackend::set public function Stores data in the persistent cache. Overrides CacheBackendInterface::set
MemcacheBackend::setMultiple public function Store multiple items in the persistent cache. Overrides CacheBackendInterface::setMultiple
MemcacheBackend::splitItem private function Given a single cache item, split it into multiple child items.
MemcacheBackend::timeIsGreaterThanBinDeletionTime protected function Determines if a (micro)time is greater than the last bin deletion time.
MemcacheBackend::valid protected function Determines if the cache item is valid.
MemcacheBackend::__construct public function Constructs a MemcacheBackend object.