Summary

Clock skew was implemented in #2484595: Add mechanism to support invalidateAll and deleteAll, but only uses 0.001 and it uses the wrong direction:

(($item_created + 0.001) > $last_bin_invalidation_time)

e.g. if we used 1000 ms tolerance it would be:

(($item_created + 1000) > $last_bin_invalidation_time)

- Item created at: 0 ms
- Bin invalidated at: 50 ms
- Item valid till forever [uh, okay ...]

Proper implementation:

- Item created at: 0 ms
- Bin invalidated at: 50 ms, bin invalidation time is set to 1050 ms
- Items created _till_ a time of 1051 ms are considered invalid.

As this only concerns deleteAll() which is seldom, we probably do not need 1 second of tolerance, but I think best is to make this a configurable and injectable parameter. Same host implementations can set it to 0 then.

Background

During development of #2989601: Support for cache tags natively in memcache we found out that time() has problems with how to deal with items written and invalidated at the same time and microtime() has the problem of needing exact clocks for multi-web-head environments (e.g. the sites we target mostly):

While we have decided to finally try to use microtime() instead of time() as in Drupal 7, we lost an important property: clock skew tolerance of 1000 ms and that means we opened ourselves to bad race conditions and hence potential *data corruption issues*, which is why this is major.

The main algorithm for tags is really simple():

- invalidate() => time()+1
- getCurrentChecksum() = min(time(), calculateChecksum())

and because here we just have:

- invalidate() => microtime(TRUE)
- item->created = microtime(TRUE)

all we need to do is

- invalidate() => microtime(TRUE) + $tolerance
- item->created = microtime(TRUE)

Items created within $tolerance microseconds will be invalid.

We could decide to go to as far as 1000 ms (like in Drupal 7) or play it more like the ChainedFast with 0.001 s, but best would be to make it configurable.

Comments

Fabianx created an issue. See original summary.

fabianx’s picture

To comprehend this a little bit:

  /**
   * Marks the fast cache bin as outdated because of a write.
   */
  protected function markAsOutdated() {

    // Clocks on a single server can drift. Multiple servers may have slightly
    // differing opinions about the current time. Given that, do not assume
    // 'now' on this server is always later than our stored timestamp.
    // Also add 1 millisecond, to ensure that caches written earlier in the same
    // millisecond are invalidated. It is possible that caches will be later in
    // the same millisecond and are then incorrectly invalidated, but that only
    // costs one additional roundtrip to the persistent cache.
    $now = round(microtime(TRUE) + 0.001, 3);
    if ($now > $this
      ->getLastWriteTimestamp()) {
      $this->lastWriteTimestamp = $now;
      $this->consistentBackend
        ->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
    }
  }

We really should be using this approach when writing the timestamp + configurable skew for the decision of safe vs. fast.

This does another thing (and yes it is correct):

If someone else has already written a later timestamp we _never_ write an earlier timestamp.

which is super important to fix clock skew in both directions.

Which is another reason why I think we should have a timestamp service in memcache and later in core, which we inject so that clock skew tolerance can be taken into account without having to re-implement the wheel 10x.

Cause now it seems both cache tags and deleteAll() need to implement the exact same code.

And in a way thats true, a cache tag is just one invalidation timestamp, so sure it has the same properties as the deleteAll deletion timestamp for the bin ...

fabianx’s picture

Here is a first patch based on the new invalidator service:

diff --git a/src/MemcacheBackend.php b/src/MemcacheBackend.php
index a034421..fba61f7 100644
--- a/src/MemcacheBackend.php
+++ b/src/MemcacheBackend.php
@@ -75,12 +75,13 @@ class MemcacheBackend implements CacheBackendInterface {
    * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
    *   The cache tags checksum service.
    */
-  public function __construct($bin, DrupalMemcacheInterface $memcache, LockBackendInterface $lock, DrupalMemcacheConfig $settings, CacheTagsChecksumInterface $checksum_provider) {
+  public function __construct($bin, DrupalMemcacheInterface $memcache, LockBackendInterface $lock, DrupalMemcacheConfig $settings, CacheTagsChecksumInterface $checksum_provider, TimestampInvalidatorInterface $timestamp_invalidator) {
     $this->bin = $bin;
     $this->memcache = $memcache;
     $this->lock = $lock;
     $this->settings = $settings;
     $this->checksumProvider = $checksum_provider;
+    $this->timestampInvalidator = $timestamp_invalidator;
 
     $this->ensureBinDeletionTimeIsSet();
   }
@@ -247,14 +248,14 @@ class MemcacheBackend implements CacheBackendInterface {
    * {@inheritdoc}
    */
   public function deleteAll() {
-    $this->updateBinLastDeletionTime();
+    $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin);
   }
 
   /**
    * {@inheritdoc}
    */
   public function invalidate($cid) {
-    $this->invalidateMultiple((array) $cid);
+    $this->invalidateMultiple([$cid]);
   }
 
   /**
@@ -298,7 +299,7 @@ class MemcacheBackend implements CacheBackendInterface {
    * {@inheritdoc}
    */
   public function removeBin() {
-    $this->updateBinLastDeletionTime();
+    $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin);
   }
 
   /**
@@ -344,16 +345,8 @@ class MemcacheBackend implements CacheBackendInterface {
     // to compare with.
     if (!$last_bin_deletion) {
       return FALSE;
-    }
 
-    // Clocks on a single server can drift. Multiple servers may have slightly
-    // differing opinions about the current time. Given that, do not assume
-    // 'now' on this server is always later than our stored timestamp.
-    // Also add 1 millisecond, to ensure that caches written earlier in the same
-    // millisecond are invalidated. It is possible that caches will be later in
-    // the same millisecond and are then incorrectly invalidated, but that only
-    // costs one additional roundtrip to the persistent cache.
-    return (($item_microtime + .001) > $last_bin_deletion);
+    return $item_microtime > $last_bin_deletion;
   }
 
   /**
@@ -365,49 +358,21 @@ class MemcacheBackend implements CacheBackendInterface {
    */
   protected function getBinLastDeletionTime() {
     if (!isset($this->lastBinDeletionTime)) {
-      $this->lastBinDeletionTime = $this->memcache->get($this->getBinLastDeletionTimeKey());
+      $this->lastBinDeletionTime = $this->timestampInvalidator->getLastInvalidationTimestamp($this->bin);
     }
 
     return $this->lastBinDeletionTime;
   }
 
   /**
-   * Updates the last invalidation time for the bin.
-   *
-   * @internal
-   *
-   * @return bool|\Drupal\memcache\DrupalMemcacheInterface
-   */
-  protected function updateBinLastDeletionTime() {
-    $now = round(microtime(TRUE), 3);
-
-    $return = $this->memcache->set($this->getBinLastDeletionTimeKey(), $now);
-
-    $this->lastBinDeletionTime = $now;
-
-    return $return;
-  }
-
-  /**
    * Ensures a last bin deletion time has been set.
    *
    * @internal
    */
   protected function ensureBinDeletionTimeIsSet() {
     if (!$this->getBinLastDeletionTime()) {
-      $this->updateBinLastDeletionTime();
+      $this->lastBinDeletionTime = $this->timestampInvalidator->invalidateTimestamp($this->bin);
     }
   }
 
-  /**
-   * Gets the invalidation time cache key.
-   *
-   * @internal
-   *
-   * @return string
-   */
-  protected function getBinLastDeletionTimeKey() {
-    return sprintf('bin_deletion:%s', $this->bin);
-  }
-
 }
bdragon’s picture

Status: Active » Needs work
StatusFileSize
new4.74 KB

semi-blind reroll of Fabian's patch.

bdragon’s picture

Fix / Tidy up the service definitions so usage is more clear.

bdragon’s picture

Status: Needs work » Needs review
bdragon’s picture

StatusFileSize
new12.34 KB

Update service definitions and documentation to use a better service naming scheme for the memcache timestamp invalidators.

bdragon’s picture

Remove default bin from MemcacheTimestampInvalidator's constructor to force the service definition to correctly define the bin.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Just a question, but otherwise RTBC - let's get this in!

+++ b/src/MemcacheBackend.php
@@ -36,24 +38,34 @@ class MemcacheBackend implements CacheBackendInterface {
-   * @var \Drupal\Core\Cache\CacheTagsChecksumInterface
+   * @var CacheTagsChecksumInterface|CacheTagsInvalidatorInterface
...
-   * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
+   * @param CacheTagsChecksumInterface $checksum_provider

Are IDE's now namespace aware? Before it was needed to put the full class for IDE's to find the classes.

  • bdragon authored 3b645a4 on 8.x-2.x
    Issue #2996620 by bdragon, Fabianx: Fix clock skew tolerance in...
bdragon’s picture

OK, committed.

fabianx’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.