Adding Customer Group Access Type To Turpentine+Varnish

Update: It looks like the pull request has been accepted and this adjustment has been fully merged into the base Turpentine extension!

I ran into an issue with one of the websites I work on while trying to add Varnish caching in front of their store. They used a lot a catalog price rules which meant that the price on the products would change based on the customer group for the customer viewing the page. At this point I had 2 options, setup Varnish to private cache all the products (which defeats the purpose of Varnish since it would rarely serve a cached page more than once) or figure out how to have Varnish save a page based on the customer group and serve to others with the same group. That leads me to this article where I will describe how to add a customer group access type to Turpentine and Varnish. I chose to use Turpentine because it is what I adjusted to have this but it could easily be adapted to any other Varnish cache since most of the changes are within the VCL.

Since Varnish works before PHP itself is run, the first thing we need to do is give Varnish some way of knowing the customer group. I did this by adding two event observers; one runs on customer_login and the other on customer_logout. I added the following XML to the Turpentine config.xml for these events:

<customer_login>
    <observers>
        <turpentine_customer_login_cookie>
            <class>turpentine/observer_esi</class>
            <method>setCustomerGroupCookie</method>
        </turpentine_customer_login_cookie>
    </observers>
</customer_login>
<customer_logout>
    <observers>
        <turpentine_customer_logout_remove_cookie>
            <class>turpentine/observer_esi</class>
            <method>removeCustomerGroupCookie</method>
        </turpentine_customer_logout_remove_cookie>
    </observers>
</customer_logout>

The customer_login observer is used to set a cookie containing the customer group id. It will set the cookie for the same lifetime as the frontend cookie by default which means it will expire at the same time the customer session expires. If persistent cart is enabled, we need to instead set the cookie lifetime to that value or else Varnish may cache incorrectly when the normal session expires but Magento still sees the persistent values. We set it by adding the following to the event observer class:

/**
 * Set a cookie with the customer group id when customer logs in
 *
 * Events: customer_login
 *
 * @param Varien_Object $eventObject
 * @return null
 */
public function setCustomerGroupCookie( $eventObject ) {
    $customer = $eventObject->getCustomer();
    $cookie = Mage::getSingleton('core/cookie');
    if (Mage::getStoreConfig('persistent/options/enabled')) {
        $cookie->set('customer_group', $customer->getGroupId(), Mage::getStoreConfig('persistent/options/lifetime'));
    } else {
        $cookie->set('customer_group', $customer->getGroupId());
    }
}

The customer_logout is used to delete the set cookie. We need to make sure Varnish does not continue serving the customer the cached version of the page based on their customer group after they have logged out. We add the following to the event observer class:

/**
 * Destroy the cookie with the customer group when customer logs out
 *
 * Events: customer_logout
 *
 * @param Varien_Object $eventObject
 * @return null
 */
public function removeCustomerGroupCookie( $eventObject ) {
    Mage::getSingleton('core/cookie')->delete('customer_group');
}

Pretty simple so far. Now we just need to adjust the Varnish VCL to read this new cookie and create new cached pages based on the value of it. To do that we need to adjust the vcl_hash method in the VCL. Turpentine has two templates used to generate the VCL file, one for Varnish v2 and another for v3. Unfortunately the syntax is different between these two versions so we just need to add the following somewhere in the vcl_hash function for each version template:

# Varnish 3.x
if (req.http.X-Varnish-Esi-Access == "customer_group" &&
        req.http.Cookie ~ "customer_group=") {
    hash_data(regsub(req.http.Cookie, "^.*?customer_group=([^;]*);*.*$", "\1"));
}

# Varnish 2.x
if (req.http.X-Varnish-Esi-Access == "customer_group" &&
        req.http.Cookie ~ "customer_group=") {
    set req.hash += regsub(req.http.Cookie, "^.*?customer_group=([^;]*);*.*$", "\1");
}

This tells Varnish to read the value of the customer_group cookie if the ESI access type is customer_group and the cookie is set. It then adds that value to the hash key used to determine a cache hit vs miss. Now that Varnish includes the customer group id in the hash key, it should only make a cache hit when the customer’s group matches a cached version for their group. At this point, Varnish is setup to have a new cache type for customer groups and you can begin defining ESI blocks with the customer_group access type to have them cached on a per group basis. The following step is just to add cache invalidation for the Turpentine module and is specific to it. You will need to modify your plugin in the correct spots to invalidate the cache based on events.

There are two lines that need to be modified in Nexcessnet/Turpentine/Model/Observer/Esi.php in order to invalidate the cached ESI blocks on events and give it the private ESI TTL. If you search this file for the following you should find 2 lines in this file:

if( $esiOptions[$cacheTypeParam] == 'private' ) {

These just need an OR condition added on in order to have their logic applied to the new customer_group access type. Modify them to look like the following and you should be all done:

if( $esiOptions[$cacheTypeParam] == 'private' || $esiOptions[$cacheTypeParam] == 'customer_group' ) {

You can now define customer_group when you are defining the Turpentine options on a block and it will create an ESI block cached per customer group. I used it on the price block and a few other spots where data changed based on the group and it works perfectly.

Leave a Reply

Your email address will not be published. Required fields are marked *