The Problem:

We wanted to be able to support two versions (look and feel) of the site with the same URLs for each. (Yes, we have very good reasons for this.) But we did not want to sacrifice package caching performance. Ideally, what we want is separate page cache variants for the theme.

Potential Solution:

One way of solving this problem is to use cookies to distinguish which of the two versions the client should get. Then, we can route traffic accordingly. To get page cache to store two versions of the page, we need to distinguish the keys for one version from those of the other version.

Attached is a small patch that generates cache variants based on cookies. It should be moderately extensible. While perhaps a core patch would be better suited for this sort of thing, that's not realistic for either D6 r D7 at this point. So I figure that this is the right place to implement such a feature.

Anyone have any thoughts about this method? Pros? Cons? Alternatives that don't involve massive amounts of path/q munging?

About the Patch:

The patch adds a short function to memcache.inc that transforms $cids.

function memcache_cookie_variant_cid($cid, $table) {
  global $conf;
  
  if ($table != 'cache_page') return $cid;
  
  foreach ($conf['cookie_page_cache_variants'] as $cookie => $prefix) {
    if (!empty($_COOKIE[$cookie])) {
      return $prefix . $cid;
    }
  }
  
  return $cid;
}

The function is called during cache_get/set/clear calls. In settings.php (or anything called very early in bootstap), one can specify a new variant cookie like this:

$conf['cookie_page_cache_variants'] = array('my_cookie_name' => 'my_prefix-');

This would result in a memcache key that would look something like this:

cache_page-my_prefix-http%3A%2F%2Fexample.com%2Ffoo%2Fbar
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mikeytown2’s picture

mikeytown2’s picture

Title: Feature: Cookie-based cache variants (Patch attached) » Allow a modifier for cache_page bin
FileSize
3.75 KB

define function name in settings.php.

$conf['memcache_cache_page_cid_modifier'] = 'my_function';
mikeytown2’s picture

Status: Needs review » Active
FileSize
3.77 KB

updated patch to pass the operator along.

below is an example from my settings.php file

$conf['memcache_cache_page_cid_modifier'] = 'settings_memcache_cid_modifier';
function settings_memcache_cid_modifier($cid, $op) {
  if ($op == 'get') {
    $name = 'og_oven';
    // Get Cookie
    if (isset($_COOKIE[$name]) && is_numeric($_COOKIE[$name])) {
      $nid = $_COOKIE[$name];
    }
    else {
      $nid = 0;
    }
    $cid .= '#oven-' . $nid;
  }
  elseif ($op == 'set') {
    if (empty($_GET['set_default']) && function_exists('og_oven_get_cookie')) {
      $nid = og_oven_get_cookie();
      $cid .= '#oven-' . $nid;
    }
  }
  return $cid;
}

This has other use cases; stripping useless query strings from the URL for higher cache hits, etc... If your wondering I want a cache miss if set_default is set.

mikeytown2’s picture

Status: Active » Needs review
mbutcher’s picture

Status: Active » Needs review

I like this patch and am testing it out. It seems to meet our requirements nicely. And as you pointed out, it can be adapted to a wide variety of similar cases. I'll update the ticket when I've finished reviewing.

mbutcher’s picture

Maybe it'd be better to factor the common logic from the three memcache.inc functions into its own function like this (untested):

/**
 * Modify the CID if necessary.
 *
 * @param $cid
 *  The cache ID.
 * @param $table
 *  The table name.
 * @param $op
 *  The operation (e.g. get, put, clear)
 * @return
 *  The CID that should be used to retrieve from the cache.
 */
function memcache_modify_cid($cid, $table, $op = 'get') {
  $modifier = variable_get("memcache_cache_page_cid_modifier", FALSE);
  if (!empty($modifier) && $table == 'cache_page' && function_exists($modifier)) {
    $cid = $modifier($cid, $op);
  }
  return $cid;
}

And then call this function in the get/set/clear functions.

This would probably be more maintainable over the long term.

mbutcher’s picture

Status: Needs review » Reviewed & tested by the community

Tested and working.

catch’s picture

Apart from early bootstrap (and we could check current bootstrap phase there), is there a reason not to do this as a hook?

Could have hook_memcache_cid_alter(), and it wouldn't hurt to have hook_memcache_bin_alter() - for example to route certain items to the cache_bootstrap bin.

mikeytown2’s picture

cache_bootstrap bin; looking to backport some D7 magic? Sounds interesting. So in order for this to work the module has to declare a hook_boot?

_drupal_bootstrap() does the cache call before require_once './includes/module.inc'; is ran. This would require a require_once call for every cache operation; or hack core to move this file above the page_get_cache();. How do you see it done?

catch’s picture

The idea would be to cache something like field info in the cache_bootstrap bin - however this is both requested and cached after full bootstrap has occurred, so no need to define hook_boot().

Existing stuff that's sent to cache_bootstrap I don't see any particular reason to mess with, or at least the feature could be added without supporting that.

Also I don't have a real use case for this yet, it was just an example ;)

mikeytown2’s picture

updated the patch to the latest dev version of memcache

avergara’s picture

This could be something that I am really looking for. But the problem is, we are using APC instead of memcache. Can anyone guide me how I can implement this on APC? Thanks.

avergara’s picture

Status: Reviewed & tested by the community » Active
mikeytown2’s picture

Status: Active » Reviewed & tested by the community

@avergara
I would start an issue over here http://drupal.org/project/apc

Jeremy’s picture

Status: Reviewed & tested by the community » Needs work

Sorry, this is an old ticket -- but, before it can be merged it needs to include an update to the README to document the new feature.