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.
| Comment | File | Size | Author |
|---|---|---|---|
| #44 | 3032078-40-multiwebhead-twig-hash-test-only.patch | 3.64 KB | catch |
| #40 | interdiff-29-40.txt | 840 bytes | jrglasgow |
| #40 | 3032078-40-multiwebhead-twig-hash.patch | 5.04 KB | jrglasgow |
| #30 | interdiff-19-29.txt | 1.25 KB | jrglasgow |
| #30 | 3032078-29-multiwebhead-twig-hash.patch | 4.92 KB | jrglasgow |
Issue fork drupal-3032078
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
Comment #2
dagomar commentedComment #3
dagomar commentedI'm attaching 2 patches, one with the test only, which should fail. The second contains the fix and the test should pass.
Comment #4
dagomar commentedComment #6
idebr commentedThis was reported earlier in #2882941: Twig cache file names in a load-balancer setup
Comment #7
dagomar commented@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?
Comment #8
idebr commentedClosed #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.
Comment #9
berdirUnfortunately, 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.
Comment #10
seanbWe could use
sha1_file()if that is acceptable? One thing that might be an issue,md5_file()/sha1_file()are significantly slower thanfilemtime(). Not sure how big that problem is though.Comment #11
dagomar commentedHi @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
Comment #15
iainp999 commentedI 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.
Comment #16
berdirYeah, 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.
Comment #17
ndf commentedcomment still has
mtimein 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).
Comment #18
anmolgoyal74 commentedCS issues still need to be handled.
Comment #19
nikitagupta commented#18 CS error is unrelated to this issue.
Comment #20
ndf commentedAs 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()tosha1_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:
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.
Comment #21
technoveltyco commentedI 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.
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.
Comment #22
jonas139 commentedI'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.
Comment #24
catchThis 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()).
Comment #25
eric_a commentedIs this normal bug somehow a duplicate of the major task #2979669: Follow-up for #2752961: automatically deleting compiled Twig templates across multiple webheads?
Comment #26
dwwWe seem to have run into this bug on a client site. Will try to review next week. Tagging to be smashed. 😉
Comment #27
sidharrell commentedWe 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.
Comment #30
jrglasgow commentedI have reworked the patch to use hash_file() and crc32 as @catch recommended
Comment #40
jrglasgow commentedattempting to fix the testing error
Comment #41
lbodiguel commented#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 !
Comment #42
cilefen commentedBecause 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.
Comment #43
nod_If it's a 9+ patch makes sense to keep the current hashing, maybe a followup for the new functions available for D10+?
Comment #44
catchxxHash 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.
Comment #45
catchErrr it can stay RTBC though!
Comment #48
catchFail patch came back properly.
Committed/pushed to 10.1.x, cherry-picked back through to 9.4.x, thanks!