Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lcache wildcard prefix clear removes all cache entries for the bin #1

Open
LionsAd opened this issue Nov 26, 2016 · 27 comments
Open

lcache wildcard prefix clear removes all cache entries for the bin #1

LionsAd opened this issue Nov 26, 2016 · 27 comments

Comments

@LionsAd
Copy link

LionsAd commented Nov 26, 2016

diff --git a/sites/all/modules/contrib/lcache/lcache.cache.inc b/sites/all/modules/contrib/lcache/lcache.cache.inc
index c789454..daa1976 100644
--- a/sites/all/modules/contrib/lcache/lcache.cache.inc
+++ b/sites/all/modules/contrib/lcache/lcache.cache.inc
@@ -63,8 +63,8 @@ class LCache implements DrupalCacheInterface {
     $this->integrated = new \LCache\Integrated($l1, $l2, 100);
   }
 
-  protected function getAddress($cid=NULL) {
-    return new \LCache\Address($this->bin, $cid);
+  protected function getAddress($cid=NULL, $wildcard = FALSE) {
+    return new \LCache\Address($this->bin, $cid, $wildcard);
   }
 
   /**
@@ -125,7 +125,7 @@ class LCache implements DrupalCacheInterface {
       $this->integrated->collectGarbage();
     }
     else {
-      if ($wildcard) {
+      if ($wildcard && $cid == '*') {
         $address = $this->getAddress();
         $this->integrated->delete($address);
       }
@@ -136,7 +136,7 @@ class LCache implements DrupalCacheInterface {
         }
       }
       else {
-        $address = $this->getAddress($cid);
+        $address = $this->getAddress($cid, $wildcard);
         $this->integrated->delete($address);
       }
     }
diff --git a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
index efe5d94..dbb9680 100644
--- a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
+++ b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
@@ -12,18 +12,22 @@ final class Address implements \Serializable
     protected $bin;
     /** @var string|null */
     protected $key;
+    /** @var bool */
+    protected $wildcard = false;
 
     /**
      * Address constructor.
      *
      * @param string|null $bin
      * @param string|null $key
+     * @param bool $woldcard
      */
-    public function __construct($bin = null, $key = null)
+    public function __construct($bin = null, $key = null, $wildcard = false)
     {
         assert(!is_null($bin) || is_null($key));
         $this->bin = $bin;
         $this->key = $key;
+        $this->wildcard = $wildcard;
     }
 
     /**
@@ -50,7 +54,7 @@ final class Address implements \Serializable
      */
     public function isEntireBin()
     {
-        return is_null($this->key);
+        return is_null($this->key) || $this->wildcard;
     }
 
     /**
@davidstrauss
Copy link
Member

I don't believe that will work correctly because a prefix wildcard clear isn't possible with LCache, so it escalates to a bin clear.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss That is not correct, it would work fine with above patch.

The bin clear is implemented as a prefix clear internally, so this should work fine.

Edited:

My original patch that comment was for was:

-      if ($wildcard) {
+      if ($wildcard && $cid == '*') {

and that indeed did not work.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

I updated the patch.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss Besides that full bin cache clears are a little problematic with Drupal out of the box:

e.g. cache_bootstrap variable write would nuke your registry and module_implements, too.

'cache' cache_clear_all (e.g. entity_info_cache_clear) would nuke system_cache_tables and theme registry ...

etc.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss And its perfectly fine for APCu to not have the prefix clear, but do a bin clear.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Hm, it seems that should work fine for APCu as well as that does use an iterator over the items.

I am not seeing how a bin clear is different from a prefix clear given how its implemented.

Do I miss something?

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Btw. Great work on lcache. Used it on an emergency black-friday project today and solved the problems with redis overload nicely. :)

Besides the too much caches are cleared issue right now ...

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Okay, I see the StaticL2 cache makes an assumption on the bin.

I need to fix Address::isMatch as well, so that it works correctly for the StaticL2. The rest looked good to me as they all use suffix '%' matching.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

And overall a 'bin' is as far as I can see an arbitrary concept in lcache. In the end its just the prefix of an address.

So logically there should be no difference from a:

'cache_fabian' bin vs. a 'cache:fabian' bin

if looking at it from the standpoint of an address.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Indeed that might be another way to fix this easier.

Just use the prefix as a 'bin', which lcache does not know anyway.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Ah that won't work due to the bin prefix string.

@davidstrauss
Copy link
Member

I would prefer to make the wildcard -> full bin conversion occur only in the Drupal module/include layer. It looks like the vendor directory got committed here, which is not correct. (I didn't set up the D7 GitHub repo but will fix it.) I would, instead, update the .inc file's factory for LCache\Address to set a wildcard to have a null $key.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Yeah, need the wildcard support in Address class; just tested.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss Any chance you can chat quickly on IRC / freenode.net?

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss The repo is fine, I am working on an emergency project, so committed vendor dir as a quick way to activate it.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss So you mean to subclass the Address class and extend it just for the D7 module?

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Address is 'final' however ...

@davidstrauss
Copy link
Member

All I'm suggesting is constructing an Address with a null $key whenever you need a wildcard. The only place the $wildcard attribute gets used is for the "entire bin" check, which can be triggered with a null $key. Anywhere else reading the key would be confused by a key with a wildcard, anyway.

The bottom line is that I'm resistant to having Drupal's needs leak into LCache in this way.

@davidstrauss
Copy link
Member

davidstrauss commented Nov 27, 2016 via email

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

i don't want a wildcard => full bin conversion.

I want the opposite, I need wildcard / prefix cache clears working.

I have patch for isMatch now, which is overly optimistic, but works fine:

    public function isMatch(Address $address)
    {
        if (!is_null($address->getBin()) && !is_null($this->bin) && $address->getBin() !== $this->bin) {
            return false;
        }
        if ($address->isEntireBin() || $this->isEntireBin()) {
            // @todo Check for wildcards.
            return true;
        }
        if (!is_null($address->getKey()) && !is_null($this->key) && $address->getKey() !== $this->key) {
            return false;                    
        }                                    
        return true;                         
    }

e.g. if using StaticL2 it would see any prefix clear as a full bin clear, but not with any other cache backend that supports true prefixes.

And yes it feels like a hack, but I need prefix clears working one way or another.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

@davidstrauss The module currently says that wildcache clears are full bin clears, but that is
problematic as explained above. I am seeing a real performance impact on a live site from that.

  • variable_set's sometimes happen and then registry cache and module_implements cache is gone
  • entity_info_cache_clear() is called (correctly) by something and that kills the whole 'cache' bin including the really important system_cache_tables entry and the theme registry and some more other caches.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

It might be fine for the D7 module to have its own Address class with its own needs (wildcards).

However, then it would be nice if upstream Address wasn't final as its a lot of code to copy, which defeats the purpose of the nice library.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Yeah, I see the problem with my approach the $this->wildcard gets lost during serialization, which is used for L1Cache.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

Ah, I got it! :)

The difference between a wildcard clear and a normal clear is a suffix of ":" per convention.

And if no suffix is used, then I can still fallback to a normal full bin clear.

diff --git a/sites/all/modules/contrib/lcache/lcache.cache.inc b/sites/all/modules/contrib/lcache/lcache.cache.inc
index c789454..602b7be 100644
--- a/sites/all/modules/contrib/lcache/lcache.cache.inc
+++ b/sites/all/modules/contrib/lcache/lcache.cache.inc
@@ -125,7 +125,7 @@ class LCache implements DrupalCacheInterface {
       $this->integrated->collectGarbage();
     }
     else {
-      if ($wildcard) {
+      if ($wildcard && ($cid == '*' || substr($cid, -1) !== ':')) {
         $address = $this->getAddress();
         $this->integrated->delete($address);
       }
diff --git a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
index efe5d94..a49a513 100644
--- a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
+++ b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
@@ -50,7 +50,7 @@ final class Address implements \Serializable
      */
     public function isEntireBin()
     {
-        return is_null($this->key);
+        return is_null($this->key) || substr($this->key, -1) === ':';
     }
 
     /**
@@ -72,7 +72,11 @@ final class Address implements \Serializable
     {
         if (!is_null($address->getBin()) && !is_null($this->bin) && $address->getBin() !== $this->bin) {
             return false;
-        }
+       }
+       if ($address->isEntireBin() || $this->isEntireBin()) {
+           // @todo Check for wildcards.
+           return true;
+       }
         if (!is_null($address->getKey()) && !is_null($this->key) && $address->getKey() !== $this->key) {
             return false;
         }

Of course this could be optimized to check for the prefix before the ':', too.

e.g. for:

foo:bar:baz

if I try to clear:

foo:bar

I could fallback to clear:

foo: instead easily.

@davidstrauss
Copy link
Member

There's a lot of nuance to LCache\Address, which is why it's final. Alterations to the behavior could very easily create bugs, at least if the alterations fail to pass the unit tests against the class.

@davidstrauss
Copy link
Member

A serialized address uses the pattern <bin-name-length>:<bin>:<key>, which allows both arbitrary characters for bin names as well as high-performance matching based on the bin name. The colon between the bin and key is for human readability.

@LionsAd
Copy link
Author

LionsAd commented Nov 27, 2016

diff --git a/sites/all/modules/contrib/lcache/lcache.cache.inc b/sites/all/modules/contrib/lcache/lcache.cache.inc
index c789454..5b3121e 100644
--- a/sites/all/modules/contrib/lcache/lcache.cache.inc
+++ b/sites/all/modules/contrib/lcache/lcache.cache.inc
@@ -63,8 +63,8 @@ class LCache implements DrupalCacheInterface {
     $this->integrated = new \LCache\Integrated($l1, $l2, 100);
   }
 
-  protected function getAddress($cid=NULL) {
-    return new \LCache\Address($this->bin, $cid);
+  protected function getAddress($cid=NULL, $is_prefix = FALSE) {
+    return new \LCache\Address($this->bin, $cid, $is_prefix);
   }
 
   /**
@@ -125,7 +125,7 @@ class LCache implements DrupalCacheInterface {
       $this->integrated->collectGarbage();
     }
     else {
-      if ($wildcard) {
+      if ($wildcard && $cid == '*') {
         $address = $this->getAddress();
         $this->integrated->delete($address);
       }
@@ -136,7 +136,7 @@ class LCache implements DrupalCacheInterface {
         }
       }
       else {
-        $address = $this->getAddress($cid);
+        $address = $this->getAddress($cid, $wildcard);
         $this->integrated->delete($address);
       }
     }
diff --git a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
index efe5d94..5dfffdb 100644
--- a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
+++ b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/Address.php
@@ -12,18 +12,22 @@ final class Address implements \Serializable
     protected $bin;
     /** @var string|null */
     protected $key;
+    /** @var bool */
+    protected $isPrefix = false;
 
     /**
      * Address constructor.
      *
      * @param string|null $bin
      * @param string|null $key
+     * @param bool $prefix
      */
-    public function __construct($bin = null, $key = null)
+    public function __construct($bin = null, $key = null, $is_prefix = false)
     {
         assert(!is_null($bin) || is_null($key));
         $this->bin = $bin;
         $this->key = $key;
+        $this->isPrefix = $is_prefix;
     }
 
     /**
@@ -50,7 +54,26 @@ final class Address implements \Serializable
      */
     public function isEntireBin()
     {
-        return is_null($this->key);
+        return is_null($this->key) || $this->isPrefix();
+    }
+
+    public function isPrefix()
+    {
+        return $this->isPrefix;
+    }
+
+    public function serializePrefix()
+    {
+        if (is_null($this->bin)) {
+            return '';
+        }
+
+        $length_prefixed_bin = strlen($this->bin) . ':' . $this->bin;
+
+        if (is_null($this->key)) {
+            return $length_prefixed_bin . ':';
+        }
+        return $length_prefixed_bin . ':' . $this->key;
     }
 
     /**
@@ -72,7 +95,11 @@ final class Address implements \Serializable
     {
         if (!is_null($address->getBin()) && !is_null($this->bin) && $address->getBin() !== $this->bin) {
             return false;
-        }
+	}
+	if ($address->isPrefix() || $this->isPrefix()) {
+	    // @todo Do real prefix check.
+	    return true;
+	}
         if (!is_null($address->getKey()) && !is_null($this->key) && $address->getKey() !== $this->key) {
             return false;
         }
@@ -98,7 +125,7 @@ final class Address implements \Serializable
 
         $length_prefixed_bin = strlen($this->bin) . ':' . $this->bin;
 
-        if (is_null($this->key)) {
+        if (is_null($this->key) || $this->isPrefix()) {
             return $length_prefixed_bin . ':';
         }
         return $length_prefixed_bin . ':' . $this->key;
diff --git a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/DatabaseL2.php b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/DatabaseL2.php
index acffecd..84d9555 100644
--- a/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/DatabaseL2.php
+++ b/sites/all/modules/contrib/lcache/vendor/lcache/lcache/src/DatabaseL2.php
@@ -250,7 +250,12 @@ class DatabaseL2 extends L2
         // Handle bin and larger deletions immediately. Queue individual key
         // deletions for shutdown.
         if ($address->isEntireBin() || $address->isEntireCache()) {
-            $pattern = $address->serialize() . '%';
+            if ($address->isPrefix()) {
+              $pattern = $address->serializePrefix() . '%';
+            }
+            else {
+              $pattern = $address->serialize() . '%';
+            }
             $sth = $this->dbh->prepare('DELETE FROM ' . $this->prefixTable('lcache_events') .' WHERE "event_id" < :new_event_id AND "address" LIKE :pattern');
             $sth->bindValue(':new_event_id', $event_id, \PDO::PARAM_INT);
             $sth->bindValue(':pattern', $pattern, \PDO::PARAM_STR);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants