Problem/Motivation

A multiple webheads setup where there is a difference between (project) file creation times causes the twig file cache folder to grow infinitely. The Twig system creates a hash with which it determines if new Twig extensions have been added/enabled, and stores this hash in state (eg one state for the site, not per webhead). Because the hash is created based on the filemtime of twig extention files, if there are multiple webheads those filemtime values might differ, causing the twig cache to be invalidated each time a request switches between webheads. The effect of this is that Twig keeps creating new files, prepending them with the new hash. Because Twig doesn't automatically clean up the cache folder this causes the twig cache storage to grow infinitely, until a manual cache clear has taken place.

To reproduce:
Have a Drupal install, copy the installation (with different url), but keep the same database credentials. Then access the site from both urls and see that twig keeps creating new hashes for twig files (file names are prepended with the hash).

Proposed resolution

Instead of using filemtime use md5_file for determining if a twig extension was added or modified.

Remaining tasks

The fix turned out to be a lot simpler than making a test for this. We had to mimick what happens in a round-robin, multi-webhead setup, which turned out to be verbose. So while the code change only contains one line, it is mostly the test that needs to be reviewed.

User interface changes

No changes to user interface.

API changes

There are no API changes.

Data model changes

There are no data model changes.

Release notes snippet

While this patch doesn't introduce any API or Data model changes, presumably md5_file is slower than filemtime, meaning that in development this might cause a slight decrease of performance.

Issue fork drupal-3032078

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

dagomar created an issue. See original summary.

dagomar’s picture

dagomar’s picture

I'm attaching 2 patches, one with the test only, which should fail. The second contains the fix and the test should pass.

dagomar’s picture

Status: Active » Needs review

idebr’s picture

dagomar’s picture

@idebr Indeed so! Despite having looked for this issue and finding several related issues this one was overlooked. I believe the patch here is more complete (includes tests) and solves the issue better because it actually checks if the file has changes as opposed to checking if the file name has changed.

What do you propose we do? Mark this one as duplicate and update that issue or other way around?

idebr’s picture

Closed #2882941: Twig cache file names in a load-balancer setup as a duplicate.

Please credit ltimmers for his work debugging this issue and providing a patch.

berdir’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
@@ -24,7 +24,7 @@ public function process(ContainerBuilder $container) {
       // and mtime for every time we change an existing file.
-      $twig_extension_hash .= $class_name . filemtime($reflection->getFileName());
+      $twig_extension_hash .= $class_name . md5_file($reflection->getFileName());
     }
 

Unfortunately, certain governments, that we shall not name here, have some weird rules about md5 being evil, even when not used for something unrelated to cryptography.

So we'll have to use a different hashing mechanism.

seanb’s picture

We could use sha1_file() if that is acceptable? One thing that might be an issue, md5_file() / sha1_file() are significantly slower than filemtime(). Not sure how big that problem is though.

dagomar’s picture

StatusFileSize
new4.68 KB
new802 bytes

Hi @seanB and @Berdir,

Thanks for the suggestions. I lost track of this issue, but I've updated the md5_file with sha1_file, although I think that if an attacker would be in a position to exploit the usage of md5_file here, you would have way bigger issues. In any case, better safe then sorry I suppose.

Sincerely,
Dagomar

Status: Needs review » Needs work

The last submitted patch, 11: multiple_webheads_twig_cache-3032078-11.patch, failed testing. View results

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

iainp999’s picture

I took a look at the test failure above. What I found was that the twig_extension_hash and twig_cache_prefix values remained consistent across the test.

However, the cache file name suffix changed for request 3.

In my case, as an example

Request 1: string(59) "5efcde6a0aabe_container.html.twig_qt9-XO_ncwZG2QwJr-bcsZySQ"
Request 2: string(59) "5efcde6a0aabe_container.html.twig_qt9-XO_ncwZG2QwJr-bcsZySQ"

Request 3: string(59) "5efcde6a0aabe_container.html.twig_OyFHPs-sIZxXLEnnNhmQ-pQsh"
Request 4: string(59) "5efcde6a0aabe_container.html.twig_OyFHPs-sIZxXLEnnNhmQ-pQsh"

What I found was that the Environment::getTemplateClass() method was generating a different key value on request 3

Request 1 and 2 : core/modules/system/templates/container.html.twigcore:escaper:optimizer:sandbox:drupal_core:debug:0:7:3:1.42.5:0:\Twig\Template:0
Request 3 and 4 : core/modules/system/templates/container.html.twigcore:escaper:optimizer:sandbox:0:7:3:1.42.5:0:\Twig\Template:0

I have highlighted the difference in bold.

In this case, I believe the difference is in the values stored in 'optionsHash' in the Environment class.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new4.45 KB
new3.83 KB

Yeah, we can't re-create the twig environment by hand like that, that doesn't have the same extensions.

Here's a different approach by unsetting the service and letting it get created again by the container builder, which I think should have a similar result. Still, I'm unsure about the test, specifically the touch() part. I don't think we should expect tests to have writable core files. I get that there isn't really any other option, but this seems super risky.

Also updated the test for D9.

ndf’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
@@ -24,7 +24,7 @@ public function process(ContainerBuilder $container) {
       // and mtime for every time we change an existing file.

comment still has mtime in it and should be adjusted. Like '... and sha1 to track file changes.'

Same comment should be adjusted in the class description too.
Currently `Parameter twig_extension_hash is a hash of all extension mtimes for Twig template invalidation.`

Note
On a high-traffic site we will use patch #16 in production. We'll report back here about performance (comment #10).

anmolgoyal74’s picture

Status: Needs review » Needs work
StatusFileSize
new88.63 KB

CS issues still need to be handled.

nikitagupta’s picture

Status: Needs work » Needs review
StatusFileSize
new4.9 KB
new1.06 KB

#18 CS error is unrelated to this issue.

ndf’s picture

Status: Needs review » Active

As noted in #17 we are using Berdir's patch #16 successfully in a high-traffic (300k/day visitors) site.
Regarding #10 and #11
Although we did not profile the performance degradation between filemtime() to sha1_file() with production-data. My expectation is that the cpu-time of these calls are neglectable compared to the total page-build time.
The calls are executed every request, but just 1 time per twig.extension. Probably not hundreds, but a few dozen max.
For what its worth; in a local-script where the calls are executed x-times each:

calls filemtime() sha1_file()
10 0.0042 s 0.0086 s
100 0.012 s 0.070 s
1000 0.093 s 0.61 s
10000 0.95 s 6.56 s
100000 9.8 s 67 s

A weaker argument is that high-traffic sites now can enable caching where before they disabled it or had to clear caches regularly.

Regarding #18
The reported CS issues are not related to this patch so the should be ignored.

Regarding #19
Code comments are now in line with the actual code.

Regarding #16
I would love to move this one to RTBC., but I find it difficult to chip in what would be a better approach for the test-coverage.
Therefor keeping this issue active.

technoveltyco’s picture

Status: Active » Needs review
StatusFileSize
new361.62 KB

I first installed the patch #19 in Drupal 8.9 and set the twig cache folder to dump in a local environment. I did the following tests, in their correspondent order, and I confirm twig flush cache worked correctly in both cases.

1. Loaded twig cache content, executed twig flush cache and checked twig cache folder. The patch still clear all the cached content in the local folder after execute it.

2. Ran the unit test provided by the patch in Drupal\KernelTests\Core\Theme\TwigEnvironmentTest::testTwigFilePrefixChange, and it passed.

Issue 3032078 patch #19 unit test execution

Based on these results, I could move the ticket to RBTC, but I consider this should be tested in a real multi webhead environment before confirming patch #19 fixes the issue.

Moving this ticket to Needs review.

jonas139’s picture

Status: Needs review » Reviewed & tested by the community

I've applied the patch on a production environment with 2 webservers and 1 loadbalancer and the creation of indefinite twig cache files is fixed! Clearing the caches in the backend and in drush is working like a charm again.

Thank you for all the work!
I'll put it on RTBC because it's tested on a real production environment like requested in the comment before.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

catch’s picture

Version: 9.2.x-dev » 9.3.x-dev
Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
@@ -23,8 +23,8 @@ public function process(ContainerBuilder $container) {
-      // and mtime for every time we change an existing file.
-      $twig_extension_hash .= $class_name . filemtime($reflection->getFileName());
+      // and sha1 for every time we change an existing file.
+      $twig_extension_hash .= $class_name . sha1_file($reflection->getFileName());
     }

This is using a weak cryptographic hash for non-cryptographic purposes. I didn't really like the change at the time, and still don't, but a long time ago in core we stopped doing that due to the number of false-positives from automated scanning software.

We should use hash_file() with crc32 instead (see for example DrupalKernel::boot()).

dww’s picture

Issue tags: +Bug Smash Initiative

We seem to have run into this bug on a client site. Will try to review next week. Tagging to be smashed. 😉

sidharrell’s picture

We are seeing consumption of up to 1G of disk space to this every 10 minutes on some of the webheads.
Recommending the patch to the customer's dev team.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

jrglasgow made their first commit to this issue’s fork.

jrglasgow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.92 KB
new1.25 KB

I have reworked the patch to use hash_file() and crc32 as @catch recommended

Status: Needs review » Needs work

The last submitted patch, 30: 3032078-29-multiwebhead-twig-hash.patch, failed testing. View results

jrglasgow’s picture

Status: Needs work » Needs review
StatusFileSize
new5.04 KB
new840 bytes

attempting to fix the testing error

lbodiguel’s picture

#40 solved the issue we encountered when a hoster delivered our website manually on two servers. Since the timestamp were different, we had ~30go of twig cache files, patch solved the issue. Many thanks !

cilefen’s picture

Because I see a crc32 implementation in this patch, and although this requires PHP 8.1, I want to highlight that there are new and faster non-cryptographic hash algorithms in PHP.

Whether these are faster in this use case must be tested.

nod_’s picture

Status: Needs review » Reviewed & tested by the community

If it's a 9+ patch makes sense to keep the current hashing, maybe a followup for the new functions available for D10+?

catch’s picture

Status: Reviewed & tested by the community » Needs review
Related issues: +#3032078: Multiple webheads can cause infinite growth of Twig cache
StatusFileSize
new3.64 KB

xxHash has an issue at #3307718: Implement xxHash for non-cryptographic use-cases.

Uploading a test-only patch. If that fails OK I agree this one is ready.

catch’s picture

Status: Needs review » Reviewed & tested by the community

Errr it can stay RTBC though!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 44: 3032078-40-multiwebhead-twig-hash-test-only.patch, failed testing. View results

  • catch committed b11e9d9 on 10.0.x
    Issue #3032078 by jrglasgow, dagomar, Berdir, nikitagupta, catch,...
  • catch committed a59f4db on 10.1.x
    Issue #3032078 by jrglasgow, dagomar, Berdir, nikitagupta, catch,...
  • catch committed d7f67e2 on 9.4.x
    Issue #3032078 by jrglasgow, dagomar, Berdir, nikitagupta, catch,...
  • catch committed a4f44cb on 9.5.x
    Issue #3032078 by jrglasgow, dagomar, Berdir, nikitagupta, catch,...
catch’s picture

Status: Needs work » Fixed

Fail patch came back properly.

Committed/pushed to 10.1.x, cherry-picked back through to 9.4.x, thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.