Fine-Grained Cache Expiration With Expire And Purge

Brad Blake
#Devops | Posted

We recently launched a site that uses Workbench Moderation for access control, the Expire ( 1.x ) and Purge modules to control cache expirations in Drupal, and Varnish in front of the web servers. However, we ran into some issues while trying to fine-tune the cache strategy.

One problem was that there weren't any options to purge pages based on values of custom entities when a node is saved. We had several custom fields that referenced terms and nodes, and needed to purge the associated entities when the node was saved.

Another problem we experienced was how Workbench Moderation was operating. In particular cases, such as publishing a new revision, the module sets all the revisions to be unpublished and creates a shutdown function that publishes the correct one. This was causing problems with caching. There were extra purge calls to Varnish, and sometimes Varnish would receive a purge and re-request the purged page before it had been published, resulting in access denied errors.

Thankfully, the Expire module defines hooks that allow developers to incorporate more fine-grained control over their cache expiration strategy.

In particular, we made use of hook_expire_cache_alter, which allows modules to alter the list of urls that will be expired. With this hook, you can do things like

  • Add urls to the expire list based on circumstances such as node type, custom entities and fields, etc
  • Remove urls from the expire list based on the same circumstances
  • Add shutdown handlers to do the expiration/purging after all the other operations from other contributed modules are completed

Here's how we can register a shutdown handler to postpone expirations until after Workbench Moderation has completed its work:

  1. function mymodule_expire_cache_alter(&$expire, $node, &$paths) {
  2. static $workbench_skipped;
  3. if (workbench_moderation_node_type_moderated($node->type) && mymodule_skip_first_save($node) && empty($workbench_skipped)) {
  4. $workbench_skipped = 1;
  5. $expire = array();
  6. }
  7. elseif (workbench_moderation_node_type_moderated($node->type)) {
  8. entity_get_controller('node')->resetCache(array($node->nid));
  9. drupal_register_shutdown_function('mymodule_expire_shutdown', $node, $expire);
  10. $expire = array();
  11. }
  12. }
  13.  
  14. function mymodule_skip_first_save($node) {
  15. if ((!empty($node->revision) ||
  16. (!empty($node->workbench_moderation_state_current) && $node->workbench_moderation_state_current != $node->workbench_moderation_state_new))
  17. && !empty($node->workbench_moderation['published'])) {
  18. return TRUE;
  19. }
  20. return FALSE;
  21. }

You can also expire pages based on conditions like node type or values of a custom field.

  1. function mymodule_expire_cache_alter(&$expire, $node, &$paths) {
  2. if (!isset($node->type)) {
  3. return;
  4. }
  5.  
  6. if ($node->type == 'homepage') {
  7. array_push($expire, '');
  8. }
  9.  
  10. $items = field_get_items('node', $node, 'field_related_term');
  11. if (!empty($items)) {
  12. for ($i = 0; $i<sizeof($items); $i++) {
  13. array_push($expire, url('taxonomy/term/' . $items[$i]['tid'], array('absolute' => TRUE)));
  14. }
  15. }
  16. }

There is also hook_expire_cache, which receives the modified list of urls to expire, and lets you take action on them. It's this hook that allows modules such as Purge to easily integrate a specific caching handler.

  1. function mymodule_expire_cache($urls) {
  2. // Action here to iterate through the urls
  3. }

So if the options available aren't the right ones for your site, or you're experiencing problems with interactions of other contributed modules, these are hooks that will save the day. If you are interested in learning more about WorkBench Moderation, check out how Tobby Hagler made Panalizer and WorkBench Moderation get along!

Brad Blake