Nov 22, 2016

Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 72 bytes)

I ran into the above error message while modifying some custom Drupal code. At first, I thought I just needed to increase the memory limit due to some newly called core functions consuming more than what was allocated.

There's good discussion about different ways to change the PHP memory limit in the article  Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)...

On my local stack, one relevant file is /etc/php5/apache2/php.ini which had a limit of 512M that I had set about two years ago. That seemed like a lot already. I upped it to 640M, but got the same error at exactly the same line of code.

So I fired up a debugger to trace through. When execution reached the loop body containing the reported line where execution died (line 4 below), I single-stepped, single-stepped, single-stepped, ... and it didn't leave after the expected number of iterations.
1:  $dirname = pathinfo($current_path, PATHINFO_DIRNAME);
2:  while ($dirname && $dirname != '.') {
3:    $page['#cache']['tags'][] = 'optimizely:' . $dirname . '/*';
4:    $dirname = pathinfo($dirname, PATHINFO_DIRNAME);
5:  }
Line 3 kept adding entries to the $page array until memory was exhausted. It was my own coding mistake in inadvertently creating a runaway infinite loop.


Sources:

Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)...
https://www.drupal.org/node/76156

Nov 3, 2016

Using Drupal 8 Cache Tags for Page Caching

This is a small case study in converting how page caching is done in the D7 version of the Optimizely module to Drupal 8.

The purpose of the module is to manage the insertion of certain <script> elements into designated pages of a site. To do so, the user creates one or more projects. Each project has one or more url paths. When a project is enabled, all pages that match one of its paths have the <script> element added.

To specify project paths, use of a trailing * wildcard is allowed, as are special page designators. For example, these are all valid paths.

/node/2
/node/*
/admin/config/system
/admin/*
*
<front>

In the case of /admin/* it would match against any of

/admin/
/admin/config/
/admin/config/system/
/admin/people
/admin/people/create
  ...

In the D7 version, invalidating is done through calls to cache_clear_all(), which takes three parameters. The function is used by the module in three different ways.

(1) To invalidate a particular page, e.g.

    cache_clear_all('/node/2', 'cache_page', FALSE);

(2) To invalidate a path with a trailing wildcard, e.g.

    cache_clear_all('/node/*', 'cache_page', TRUE);

(3) To invalidate all pages of the site,

    cache_clear_all('*', 'cache_page', TRUE);

In Drupal 8 the Cache API is completely different. Function cache_clear_all() has disappeared.

After some research, it looked like using cache tags would be the way to go. The article  Cacheability of render arrays  was especially helpful in how to think about caching as applied to page rendering.

There are two facets to implementing this. The first is what needs to be done when a page is rendered, the second is what to invalidate when triggering changes occur.

For page rendering, I already had an implementation of hook_page_attachments() that checked for inserting the element into any page whose path matched against any of the enabled project paths.

This hook function is where cache tags could be added to the page. This turned out to be a little tricky. A page must be invalidated for two different use cases: when the page contains the element which now needs to be removed, and when the page does not contain the element but it now needs to be added.

For the first case, I decided to just use the matching project path act as the cache tag (there can only be one because overlapping project paths are not allowed).

But for the second case, I had to cover all the possible project paths that might be enabled in the future. So, for example, for the page at /node/2 there are three such possible paths.

  /node/2
  /node/*
  *

For every page rendered, it was necessary to attach all of these possible project paths as cache tags. Here is a snippet of code that shows how this is done in hook_page_attachments().

  // Site-wide wildcard.
  $page['#cache']['tags'][] = 'optimizely:*';

  // Non site-wide wildcards. Repeat for every directory level.
  $dirname = pathinfo($current_path, PATHINFO_DIRNAME);
  while ($dirname && $dirname != '/') {
    $page['#cache']['tags'][] = 'optimizely:' . $dirname . '/*';
    $dirname = pathinfo($dirname, PATHINFO_DIRNAME);
  }

  // The specific page url.
  $page['#cache']['tags'][] = 'optimizely:' . $current_path;


  // Finally, if there is an alias for the page, tag it.
  if ($current_path_alias) {
    $page['#cache']['tags'][] = 'optimizely:' . $current_path_alias;
  }


The optimizely: prefix follows the convention of prefixing a group name as part of the tag where appropriate. For example, cache tags from core include node:1 and config:node.type.article.

Finally, there is the matter of what and how to invalidate when changes to the projects and their paths are submitted. This turned out to be fairly easy to implement.

An array of all relevant project paths is passed to a function that carries out the following.

  $cache_tags = [];
  foreach ($path_array as $path) {
    $cache_tags[] = 'optimizely:' . $path;
  }
 

  \Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags);



For debugging purposes, outputting X-Drupal-Cache-Tags in HTTP headers was extremely useful. See my earlier post  Enable and Use X-Drupal-Cache-Tags in HTTP headers.

This post is about the caching that is done by Drupal itself. The D7 version also checks for the presence of the varnish module and calls a function of that module if it exists.  I did not pursue a replacement for that functionality, but the articles  Varnish  and  Use Drupal 8 Cache Tags with Varnish and Purge  look promising.


Sources:

Function cache_clear_all() has been removed
https://optimizely-to-drupal-8.blogspot.com/2014/07/function-cacheclearall-has-been-removed.html

Cacheability of render arrays
https://www.drupal.org/developing/api/8/render/arrays/cacheability

Cache tags
https://www.drupal.org/developing/api/8/cache/tags

Allow to set #cache metadata in hook_page_attachments() https://www.drupal.org/node/2475749

public static function Cache::invalidateTags
https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Cache!Cache.php/function/Cache%3A%3AinvalidateTags/8