--- /dev/null
+<?php
+
+namespace Drupal\memcache\Cache;
+
+use Drupal\Core\Cache\CacheTagsChecksumInterface;
+use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+use Drupal\memcache\Invalidator\TimestampInvalidatorInterface;
+
+/**
+ * Cache tags invalidations checksum implementation by timestamp invalidation.
+ */
+class TimestampCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface {
+
+ /**
+ * The timestamp invalidator object.
+ *
+ * @var \Drupal\memcache\Invalidator\TimestampInvalidatorInterface
+ */
+ protected $invalidator;
+
+ /**
+ * Contains already loaded cache invalidations from the backend.
+ *
+ * @var array
+ */
+ protected $tagCache = [];
+
+ /**
+ * A list of tags that have already been invalidated in this request.
+ *
+ * Used to prevent the invalidation of the same cache tag multiple times.
+ *
+ * @var array
+ */
+ protected $invalidatedTags = [];
+
+ /**
+ * Constructs a TimestampCacheTagsChecksum object.
+ *
+ * @param \Drupal\memcache\Invalidator\TimestampInvalidatorInterface $invalidator
+ * The timestamp invalidator object.
+ */
+ public function __construct(TimestampInvalidatorInterface $invalidator) {
+ $this->invalidator = $invalidator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function invalidateTags(array $tags) {
+ foreach ($tags as $tag) {
+ // @todo Revisit this behavior and determine a better way to handle.
+ // Only invalidate tags once per request unless they are written again.
+ if (isset($this->invalidatedTags[$tag])) {
+ continue;
+ }
+ $this->invalidatedTags[$tag] = TRUE;
+ $this->tagCache[$tag] = $this->invalidator->invalidateTimestamp($tag);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrentChecksum(array $tags) {
+ // @todo Revisit the invalidatedTags hack.
+ // Remove tags that were already invalidated during this request from the
+ // static caches so that another invalidation can occur later in the same
+ // request. Without that, written cache items would not be invalidated
+ // correctly.
+ foreach ($tags as $tag) {
+ unset($this->invalidatedTags[$tag]);
+ }
+ // Taking the minimum of the current timestamp and the checksum is used to
+ // ensure that items that are not valid yet are identified properly as not
+ // valid. The checksum will change continuously until the item is valid,
+ // at which point the checksum will match and freeze at that value.
+ return min($this->invalidator->getCurrentTimestamp(), $this->calculateChecksum($tags));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($checksum, array $tags) {
+ if (empty($tags)) {
+ // If there weren't any tags, the checksum should always be 0 or FALSE.
+ return $checksum == 0;
+ }
+ return $checksum == $this->calculateChecksum($tags);
+ }
+
+ /**
+ * Calculates the current checksum for a given set of tags.
+ *
+ * @param array $tags
+ * The array of tags to calculate the checksum for.
+ *
+ * @return int
+ * The calculated checksum.
+ */
+ protected function calculateChecksum(array $tags) {
+
+ $query_tags = array_diff($tags, array_keys($this->tagCache));
+ if ($query_tags) {
+ $backend_tags = $this->invalidator->getLastInvalidationTimestamps($query_tags);
+ $this->tagCache += $backend_tags;
+ $invalid = array_diff($query_tags, array_keys($backend_tags));
+ if (!empty($invalid)) {
+ // Invalidate any missing tags now. This is necessary because we cannot
+ // zero-optimize our tag list -- we can't tell the difference between
+ // a tag that has never been invalidated and a tag that was
+ // garbage-collected by the backend!
+ //
+ // This behavioral difference is the main change that allows us to use
+ // an unreliable backend to track cache tag invalidation.
+ //
+ // Invalidating the tag will cause it to start being tracked, so it can
+ // be matched against the checksums stored on items.
+ // All items cached after that point with the tag will end up with
+ // a valid checksum, and all items cached before that point with the tag
+ // will have an invalid checksum, because missing invalidations will
+ // keep moving forward in time as they get garbage collected and are
+ // re-invalidated.
+ //
+ // The main effect of all this is that a tag going missing
+ // will automatically cause the cache items tagged with it to no longer
+ // have the correct checksum.
+ foreach ($invalid as $invalid_tag) {
+ $this->invalidator->invalidateTimestamp($invalid_tag);
+ }
+ }
+ }
+
+ // The checksum is equal to the *most recent* invalidation of an applicable
+ // tag. If the item is untagged, the checksum is always 0.
+ return max([0] + array_intersect_key($this->tagCache, array_flip($tags)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reset() {
+ $this->tagCache = [];
+ $this->invalidatedTags = [];
+ }
+
+}