Problem/Motivation

Core has a MemoryCache implementation, which provides the same API as cache backends but with everything held in PHP memory.

This is already used for the entity static cache after #1596472: Replace hard coded static cache of entities with cache backends.

The long term goal is to solve #2558857: Migrations invalidate entity caches when trying to reclaim memory, should flush 'automatically' - i.e. that core would statically cache things, but that our implementation would prevent memory leaks without intervention. This will solve a lot of issues with processes that need to load lots of entities, with migrate as the main core example #2558857: Migrations invalidate entity caches when trying to reclaim memory, should flush.

Back in 2011, @catch suggested an implementation using weakreferences - at that time only a PECL extension #1237636-7: Entity lazy multiple front loading.

There's an issue open to add a basic LRU cache #1199866: Add an in-memory LRU cache implementation (also from 2011), but that has limitations in that there has to be a hard-coded numeric limit on the entities added, which could still lead to out of memory issues in some situations, or entities being ejected from the cache when there's plenty of memory left.

WeakReferences are now in PHP 7.4: https://www.php.net/manual/en/class.weakreference.php

WeakMap is now in PHP 8: https://www.php.net/manual/en/class.weakmap.php / https://wiki.php.net/rfc/weak_maps (can't find actual PHP 8 docs for it yet). With a PHP 7.4 polyfill https://github.com/BenMorel/weakmap-polyfill

Since Drupal 10 will require PHP 8 or higher, we could add an implementation to core in a 9.x minor release, then Drupal 10 could switch the default for entity caching to it.

Steps to reproduce

Load thousands of entities in one go.

Proposed resolution

Start with a WeakReference implementation for now.

WeakMaps may end up being useful for entity or other caching, but this will require further investigation.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

CommentFileSizeAuthor
#39 reroll_diff_3190992_33-39.txt2.02 KBankithashetty
#39 3190992-39.patch10.21 KBankithashetty
#33 interdiff-34.txt478 bytesKapilV
#33 3190992-34.patch10.21 KBKapilV
#26 3190992-26.patch10.21 KBcatch
#26 3190992-interdiff.txt1.69 KBcatch
#25 3190992-25.patch9.96 KBcatch
#25 3190992-25-interdiff.txt734 bytescatch
#23 3190992-23.patch9.78 KBcatch
#22 3190992-22.patch9.78 KBcatch
#22 3190992-22-interdiff.txt592 bytescatch
#21 3190992-21.patch9.77 KBcatch
#20 3190992-20.patch9.78 KBcatch
#19 3190992-19.patch9.15 KBcatch
#19 3190992-19-interdiff.txt1.65 KBcatch
#18 3190992-18.patch7.97 KBcatch
#18 3190992-interdiff.txt710 bytescatch
#17 3190992-17.patch7.93 KBcatch
#17 3190992-interdiff.txt3.25 KBcatch
#16 interdiff_15-16.txt1.05 KBSuresh Prabhu Parkala
#16 3190992-16.patch5.14 KBSuresh Prabhu Parkala
#15 3190992-15.patch5.14 KBneclimdul
#15 3190992-15.interdiff.txt1.78 KBneclimdul
#14 interdiff_12-14.txt882 bytesravi.shankar
#14 3190992-14.patch4.74 KBravi.shankar
#12 interdiff_10_12.txt2.14 KBanmolgoyal74
#12 3190992-12.patch4.75 KBanmolgoyal74
#10 3190992-10.patch4.72 KBranjith_kumar_k_u
#9 3190992-9.patch4.72 KBcatch
#8 1199866-8.patch4.72 KBcatch
#7 3190992-7.patch4.86 KBcatch
#5 3190992-5.patch3.48 KBcatch
#5 3190992-interdiff-4-5.txt684 bytescatch
#4 3190992-4.patch3.48 KBcatch
#4 3190992-3-4-interdiff.txt716 bytescatch
#3 3190992-3.patch3.48 KBcatch
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch created an issue. See original summary.

heddn’s picture

Note: the memory issues in #2558857: Migrations invalidate entity caches when trying to reclaim memory, should flush are not 100% resolved with clearing entity cache. Drush and Drupal slowly seem to eat up and not release more memory over time with migrations. But once we land that issue, it will be much easier to see any other places for improvement. Baby steps.

catch’s picture

Here's an (untested) start.

While this will help with memory, it doesn't solve it completely, because even when references to the entities are cleaned up, we're still storing an array of stdClass pointing to empty WeakReferences.

Pretty sure this is what WeakMap is supposed to help with, but I don't see a way to make that work with our caching and entity systems yet.

catch’s picture

British English...

catch’s picture

Can't use the constructor.

catch’s picture

Title: Add a WeakRef memory cache implementation » Add a WeakReference memory cache implementation
Issue summary: View changes
catch’s picture

Adding some very basic unit testing. Can't get gc_collect_cycles() to clear the WeakReferences yet - not sure if it's me, or if it's not really testable.

catch’s picture

Minor clean-up.

catch’s picture

Cspell didn't like the test content.

ranjith_kumar_k_u’s picture

Updated the test content

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/tests/Drupal/Tests/Core/Cache/WeakReferenceMemoryCacheTest.php
    @@ -0,0 +1,37 @@
    + * @coversDefaultClass \Drupal\Core\Cache\MemoryCache\LruMemoryCache
    

    Wrong class name

  2. +++ b/core/tests/Drupal/Tests/Core/Cache/WeakReferenceMemoryCacheTest.php
    @@ -0,0 +1,37 @@
    +class WeakReferenceMemoryCacheTest extends UnitTestCase {
    

    Shouldn't this extend \Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase? (And be a kernel test)

anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
4.75 KB
2.14 KB

Addressed #11

catch’s picture

Status: Needs review » Needs work

@anmolgoyal74 #11.2 recommends extending GenericCacheBackendUnitTestBase, but that change isn't in the most recent patch - there should be other cache backend tests extending this same one to compare against. Also still needs the class name in #11.1 updating.

ravi.shankar’s picture

neclimdul’s picture

Here's a more functional version of the test that implements the required abstract method.

The base test catches one bug that I fixed. The current patch assumes the data to be an object but its mixed so only passing objects to weak reference seems to be the answer.

There are still some failures that I didn't really look too closely at yet. It looks like maybe the generic test is asserting some PHP pass by value or return by value behaviors that might be a bit different for weak references.

Suresh Prabhu Parkala’s picture

Tried to fix custom command failures. Please review.

catch’s picture

So the remaining failure is due to the following, as hinted at by @neclimdul.

Regular cache backends serialize and unserialize objects, meaning if you request an object, change something on it, then request it again from cache, you should get the 'same' (or rather, a copy of the...) object back without the interim modification.

For a 'static cache' backend, we want the opposite behaviour, since we're replacing 'chuck an object on a property of a class' as opposed to 'store an object in the database for a later request'.

I think we can probably still use the base class, but need to adapt things for our needs a bit.

catch’s picture

catch’s picture

The testing for invalidate tags relies on the cache invalidator service (i.e. Cache::invalidateTags()), but that isn't set up for memory cache backends so overriding the method to test it directly. This isn't necessarily the best way to deal with this problem, but also not convinced this is the issue to refactor the cache testing base class.

catch’s picture

Better way to deal with the cache invalidator via @berdir.

catch’s picture

catch’s picture

catch’s picture

Whitespace again...

bradjones1’s picture

I spent a good amount of time tonight reviewing this manually and also running the new test methods locally. Only nit I have from a DX standpoint here is that the test would be more helpful, perhaps, to people new to the WeakReference concept if the docblock on ::testSetGetNoReference() explained more about why the test is skipped; that is, the object in the cache *will* change if you alter its properties after retrieving it.

Might be a stupid question, but is it possible that someone might be relying on the existing behaviour, which guards against this via serialization? Does this need a change record to note that this is truly a reference to an object in a static cache, vs. a repository that stores the object as it was when set?

catch’s picture

Added a note to the test class. On the second point that is covered already by MemoryCacheInterface, but added an @see to that.

catch’s picture

@berdir asked a question in slack - i.e. whether weakreferences are destroyed only by garbage collection, or immediately when unset/they got out of scope.

The rfc suggests garbage collection: https://wiki.php.net/rfc/weakreferences - this is the behaviour we want.

The php docs say when they're unset/out of scope: https://www.php.net/manual/en/class.weakreference.php

Added test coverage for this, and it looks like bad news to me :/

Wim Leers’s picture

This will be great to have in Drupal core! I for sure ran into problems related to this while working on migrations!

alexpott’s picture

I guess given #26 we're back to LRU as often an entity will not remain in scope so the entity memory cache wouldn't really be helped by this at all.

alexpott’s picture

I posted https://bugs.php.net/bug.php?id=80951 - let's see if that get's any traction.

alexpott’s picture

I posted https://bugs.php.net/bug.php?id=80951 - let's see if that get's any traction.

catch’s picture

Status: Needs review » Needs work

'not a bug' on https://bugs.php.net/bug.php?id=80951.

This makes them pretty useless for our purposes. An individual method that's creating thousands of objects could use them, but how many of those are there really?

Moving to 'needs work' so we can discuss a bit more, but I think we're back to #1199866: Add an in-memory LRU cache (which I was hoping to leapfrog entirely with this this issue, turns out not).

KapilV’s picture

Assigned: Unassigned » KapilV
KapilV’s picture

Assigned: KapilV » Unassigned
Status: Needs work » Needs review
FileSize
10.21 KB
478 bytes

Fixed Custom Commands Failed.

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

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

geek-merlin’s picture

> This makes [weakrefs] pretty useless for our purposes.

Interesting problem. What if we use refcycle black magick to save the weakref from the refcounter. PHP has a hardcoded limit of 10k refcycles before auto-gc, so doing that for cache items is pretty useless, but if we weakref the cache service itself, the limit should be OK for now. "10k cache services are enough for everyone".

geek-merlin’s picture

Here's a Proof Of Concept that and how #34 can be done:
https://3v4l.org/MG8tb

rgpublic’s picture

Hm, geek-merlin's idea is certainly cool. Although I came to wonder why we are relying on PHP's garbage collection anyway. With its hardcoded 10k value it seems somewhat arbitrary anyway when things get purged.

We could also have 2 different bins. In one bin go normal/non-weak/"strong" references in the other bin go the weak references. At first if I set sth., it goes to the strong bin together with a value. It could be a timestamp or it could even be the number of garbage cycles (gc_status) to emulate PHP's garbage collection behavior. And then if sth. new goes in, we always compare whether some stuff in the strong bin should go to the weak bin (if some time has passed, some garbage cycles took place, whatever). This way, the conditions WHEN that actually happens could be a bit more predictable and configurable...

Just thinking out loud - ignore me if it sounds too crazy ;-)

mstrelan’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

I tested #33 on a batched post update hook that was running out of memory loading ~1500 nodes in batches of 50. Before the patch memory usage was increasing a few MB with every batch, after the patch the memory usage remains pretty much constant. The patch no longer applies to 9.2.x or 9.3.x.

ankithashetty’s picture

Rerolled the patch in #33 as requested in #38, thanks!

Wim Leers’s picture

@mstrelan: thanks for the super valuable real-world testing!

mstrelan’s picture

I should add that the update was executed via drush in case that makes a difference.

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

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

acbramley’s picture

We were running into memory issues on our hosted environments (the dreaded The process has been signaled with signal "9". error) when migrating 3.5k entities (for some reason locally drush would correctly create a new thread).

With patch #39 we no longer had new threads created locally and the upgrade ran flawlessly on our hosted environment!

longwave’s picture

Version: 10.0.x-dev » 10.1.x-dev
catch’s picture

Huh #44 suggests this might actually work for our purposes when the PHP docs were discouraging at best, that would be extremely good news if so!

longwave’s picture

Or, it was effectively acting as a null cache backend, which solved the memory problem as it wasn't actually caching anything?

catch’s picture

OK I just checked the test failures in #39 and it's the same problem as
#26-31 (i. the test is OK and the code it's testing OK, but weakreferences aren't doing what we want). The weakreferences don't persist when the object goes out of scope, which means there's no persistence across a request, which is not useful for entity caching. So the patch is likely fixing @acbramley's memory issues because it's essentially disabling the entity static cache on the cli.

acbramley’s picture

-_- damn

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture