Problem/Motivation
If you need to load a lot of entities and iterate over them (pretty common), your memory usage could easily go through the roof.
For example, consider updating all nodes for some reason is a post_update hook. You want to load them all and save them, but loading all 1_000_000 nodes will over time lead to a out of memory issue on the machine. In order to solve that one common strategy would be, to load them in batches and in between clear the static memory cache.
This could also affect core directly, as we may need to update content (or config) entities in the upgrade path, but have no way of knowing how many items a site will have. This could cause problems and easily balloon over the PHP requirements we set to run Drupal.
Proposed resolution
Introduce an EntityIterator class that uses a generator to break the IDs to load into chunks. Then load just a set amount each time, clearing the static cache before each load so it only contains the items currently being iterated over.
We can get clever and use a generator. Note: We clear the memory cache using $memory_cache->deleteAll() so all referenced entities are removed from the cache as well.
Remaining tasks
User interface changes
N/A - Developer only
API changes
Addition of new Iterator. Example usage:
$iterator = new ChunkedIterator($entity_storage, \Drupal::service('entity.memory_cache'), $all_ids);
foreach ($iterator as $entity) {
// Process the entity
}
Data model changes
N/A
| Comment | File | Size | Author |
|---|---|---|---|
| #136 | MR439_4ffcf833.diff | 12.86 KB | bhanu951 |
| #116 | interdiff.txt | 3.84 KB | lauriii |
| #116 | 2577417-116.patch | 7.65 KB | lauriii |
| #113 | interdiff_111-113.txt | 682 bytes | pooja saraah |
| #113 | 2577417-113.patch | 8.01 KB | pooja saraah |
Issue fork drupal-2577417
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:
- 2577417-add-an-entity
changes, plain diff MR !439
- 2577417-entity-id-iterator-poc
compare
Comments
Comment #2
dawehnerLove the idea! When we have a paging generator for the query we can achieve quite some stuff.
Comment #3
damiankloip commentedComment #4
damiankloip commentedThis works pretty well initially.
Comment #5
dawehnergenerator are everywhere!
Comment #6
damiankloip commented- webflo (IRC) 2015
Comment #7
dawehnerIMHO this is at least a task
Comment #8
bojanz commentedGreat idea!
Comment #9
webflo commentedI found a related project in D7 contrib https://www.drupal.org/project/entity_process_callback
Comment #10
catchShould be useful for CMI config entities as well as config ones. I wrote an iterator for 6.x Views at #853864: views_get_default_view() - race conditions and memory usage back in 2011 which still hasn't been committed :P
We've fixed the bulk of the issues in Views that patch was trying to solve, but for config updates we'll still need to load all and save, and there are potentially places like field info it could help as well.
I think this get more and more useful as we start to move away from batch API. For CLI you want to do everything in one process, and ideally wrap large operations like this in a transaction. So if we can write code that works in long running processes correctly like this, then use batch only to handle timeouts and user feedback, we'll be in a much better state overall.
Comment #11
catchComment #12
damiankloip commentedWith some quick unit tests.
Comment #13
wim leersI wonder if there is a common term for this in the world of software outside of Drupal?
Wouldn't something like
chunkSizemake more sense?Comment #14
damiankloip commentedWe can see if there is a more widespread name for this kind of iterator. Here is the property rename for now - makes sense to me.
Comment #15
catchI did a quick google for 'chunked iterator' and there at least people using that term on the internet, especially the last one is exactly the same issue we're dealing with here.
http://stackoverflow.com/questions/7926908/chunking-an-iterable
https://vinaybalamuru.wordpress.com/2012/05/14/basic-chunked-list-iterator/
I didn't send much time looking for other terms, worth doing but it wouldn't only be us at least if we stick with chunked iterator.
Comment #16
dawehnerWhat I don't understand is why we need an iterator at all. Can't we just make a function which has the yield statement, aka. acts directly as generator and be done with it?
This generator would then implement \Traversable which is mostly all we need? Countable could then be something on top of that.
Comment #17
damiankloip commentedIf we use an iterator we can inject the storage. Otherwise aren't we in \Drupal::entityManager hell? I would rather have it this way I think... maybe I don't understand what you're saying completely.
Comment #18
damiankloip commentedOh yeah, I guess we could just pass it as another parameter. count() is then awkward though, as you would need to do all the actual loading etc.. Plus I think adding functions is kind of against what people are expecting now. An iterator class will feel more at home I think?
Comment #19
dawehnerWell sure, let's get this in, its progress!
I think you should be able to achieve the same by using a class with a method in which you yield all the things.
Comment #20
xjmAs an API addition, this should now go into 8.1.x as per https://www.drupal.org/core/d8-allowed-changes#minor. Thanks @damiankloip and @dawehner!
Comment #21
alexpottWould it not just be better to have a way of loading entities without static caching at all? I think it is weird to blow the entire static cache even for entities not loaded through the ChunkedIterator.
Comment #22
damiankloip commentedThat would be nice. Trying to add that at this stage seems like quite a bit of work though? Would we add a new method? I guess we would have to. But then any existing implementations would not implement it etc... who knows what people are already doing with entities.
This was going for just a simple helper that people can use if they like. The ChunkedIterator could just reset the cache of the items it loaded..
Comment #23
dawehnerHere is an idea, let's clone the entity storage object in the constructor. With that we no longer clear the cache on that particular object, so it doesn't affect the actual mostly used entity storage object.
Comment #24
damiankloip commentedI like that idea, let's try that.
Comment #25
damiankloip commentedAlso tested manually, seems to work fine.
Comment #26
aheimlich\Drupal\Core\Entity\ContentEntityStorageBase::resetCache()clears both the static cache and the persistent cache. I can understand the iterator wanting to clear the static cache between chunks, but is clearing the persistent cache as well a good idea? I would think not, for the same reasons as #23.Comment #27
damiankloip commentedHuh? When the hell did it start doing that? That method was always meant for clearing static caches only.
EDIT: looks like the docs support that thinking too.
Comment #28
aheimlichContent Entity storage seems to have had this feature ever since #597236: Add entity caching to core.
\Drupal\Core\Entity\ContentEntityDatabaseStoragegot it in the aforementioned issue. It was retained when\Drupal\Core\Entity\ContentEntityDatabaseStoragewas renamed to\Drupal\Core\Entity\Sql\SqlContentEntityStoragein #2330091: Rename ContentEntityDatabaseStorage to SqlContentEntityStorage. It was then moved to\Drupal\Core\Entity\ContentEntityStorageBasein #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks.Comment #29
damiankloip commentedThorough. Thanks!
The docs on the interface are still completely misleading then. Which means the content entity implementation is not really playing by the rules.
It seems seriously restricting (and wrong) to not be able to clear the static cache without clearing the persistent cache.
https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!ContentEnt...
"Resets the internal, static entity cache."
Comment #30
bojanz commentedWe should definitely have a way to only reset the static entity cache. I had no clue this resulted in a persistent cache reset.
Comment #31
damiankloip commentedYeah, it's pretty bad IMO. A misuse of the current interface.
Comment #32
catch#2558857: Migrations invalidate entity caches when trying to reclaim memory, should flush has the same problem.
Also see #1596472: Replace hard coded static cache of entities with cache backends, #375494: Restrict the number of nodes held in the node_load() static cache, #1199866: Add an in-memory LRU cache.
Comment #33
damiankloip commentedI guess it could be solved by having a whole static cache backend injected or something. That will still get awkward to invalidate though.
It's really* frustrating that all of these problems have spawned from a complete mistake in the content storage implementation. Either way I think that needs to be fixed. The current working is wrong but also makes things really inconsistent between entity implementations.
Comment #34
catchSo only the storage and tests themselves should have to worry about the persistent entity cache - just saving and updating field definitions etc. shouldn't need to reset caches at all. Feels like something we can straightforwardly change in a minor release at least.
So I think it's worth just deleting that code, moving it into ->save() somewhere, and seeing what breaks - probably separate issue to tackle that bit?
Comment #35
damiankloip commentedIn theory that sounds workable. I'll have a look and see what breaks... :)
Comment #36
damiankloip commentedCreated #2635440: Document what cache clearing from ContentEntityStorageBase::resetCache() actually clears for this.
Comment #40
eclipsegc commentedSo, no movement on this issue in quite a while, but I'd very much like to see us approach this topic again as yields and generators are totally something we should be actively taking advantage of.
Phenaproxima and I were discussing this earlier today and in order to keep BC, he suggested a "streamMultiple()" method that can be used in leu of the current methods when yields are more favorable than returns. I'd also suggest a "streamMultipleByProperties()" method and just use raw entityFieldQuery with both of these and side-step all typical entity loading operations. I'm a little uncertain of the implications of the caching here, but since this use case is most beneficial in the scenario where we'd exhaust memory on the system by loading all the entities into memory in the first place, it seems beneficial to me to side step it completely if possible. Please feel free to disabuse me of that notion of it's senseless, I don't claim to understand all the nuance here.
Eclipse
Comment #41
phenaproxima+1 what @EclipseGc described. The idea of a specialized, chunked entity iterator, IMHO, is utterly obviated by the existence of generators. We should take advantage of this wonderful, criminally underused feature of PHP.
I propose we add an interface that can be optionally implemented by entity storages, to maintain BC:
Both of these methods would return generators.
Comment #42
dawehnerDid you had a look at the actual patch? This is really just using a generator, while it allows you to specify the chunk size. Putting more things onto the same object (entity the storage) makes it harder to maintain BC etc.
The tricky bit is really all about the reset of the caches, so you don't end up with infinite amount of used memory :)
Comment #43
damiankloip commentedYeah, the current approach already uses a generator! :) A traversable instance is returned there anyway. So maybe both look at the work that's been done here already and read the previous comments. IMO it is still better keeping this functionality in a standalone class like it is currently, it is unit tested and simple to use. If we really wanted these new methods (although entities are ridiculously overloaded already), the implementation is as easy as:
And you get your traversable object back to use.
As Daniel mentioned, it is indeed the caching that is the problem for core (all above too) - that is the crux of this issue, iterating is easy. Just to iterate a set of content entities, you currently need to clear the persistent cache too.... For an example workaround to circumvent the persistent cache clearing, see http://blog.damiankloip.net/2016/d8-entity-iterator that's an example of what you have to do due to the way the current 'API' works.
Comment #44
eclipsegc commentedSo, at the risk of getting pedantic...
Yes I read the patch. Maybe we shouldn't lead with "did you read the patch"? It's kind of belittling.
In my comment, I never suggested that the patch wasn't using generators, just that we should see movement on this basic idea again because of the value of generators, thus the reason I even bothered to comment.
To the meat of the patch itself, my comment was really just trying to say "this is great but I feel like a simpler implementation might get quicker traction." and "Is it possible to side-step the caching completely?" (which is the conversation I'd prefer to be having) I'm not trying to start fights here, just trying to figure out what the mvp for a generator based solution to entity loading might be.
As catch explained to me in irc, loadMultiple exists for a reason, and that getting individual entities is more costly from a time perspective on long runs. I totally appreciate that. However, as Damian's own comment points out, whether we're loading one entity at a time, or 50... is sort of an implementation detail in this instance, so the least effort to get a generator of any sort into this pipe-line is probably a worthwhile effort.
If we don't want to add additional methods to the Entity Storage, that's fine. Again, implementation detail.
Eclipse
Comment #45
dawehnerI have a hard time extracting what you try to tell us :)
Comment #46
eclipsegc commentedlol ok tl:dr;
Eclipse
Comment #47
dawehnerWell, keep in mind that in Drupal core MVP is tricky, as well, you have to support it for years. Putting some thoughts into the implementation, which @damiankloip did, is not a bad idea.
Comment #48
damiankloip commentedIt would be nice to keep the iterator that we have now, as it does encapsulate the logic nicely, and then can be re-used whenever/wherever needed, in pretty much any context. It's just that darn caching. Having an implementation coupled in the controller itself could allow us to directly clear the statically cached items but then we would need new interface and more methods etc.. In an ideal world I would totally stick with the current approach.
I'll have a little play around.
Comment #49
catchReminder that I have a very old issue that would fix the caching problem here. Adding to related issues.
Comment #50
damiankloip commented@catch, did you add it? Are you thinking #1596472: Replace hard coded static cache of entities with cache backends ?
Comment #51
catchYep that issue, I tried to add it as a related issue but didn't stick, trying again.
Comment #52
jibranDuplicate of this issue #2895215: Provide an implementation of loading all entities with an iterator instead of an array of entities.
Comment #56
mxh commentedI guess since the Entity Memory Cache is now a consolidated service, we could move on here. Seems the only thing we need is to add this service and use it for clearing the cache, instead of (mis-)using
$storage->resetCache().Comment #57
pwolanin commentedYes, let's see about rolling a new version of the patch.
Comment #58
hchonov\Drupal\Core\Cache\MemoryCache\MemoryCacheis shared across all storages and in order to remove a specific entity from it one must know the CID under which the entity is cached, which currently is internal to the storage.I think that a better solution will be to prevent adding the entities to the static cache in the first place. The following code should do the work:
Comment #59
ndobromirov commented#58 +1
Comment #61
pwolanin commentedre-roll using suggestion from #58 plus interdiff
Comment #62
pwolanin commentedNR please
Comment #63
pwolanin commentedLooking at Views module, which is where the pain point is for me.
Basically we need this to run in chunks in some way, basically as we iterate over
$view->result.\Drupal\views\Plugin\views\query\Sql::loadEntities()
I think what's a bit tricky there is that you don't really want to modify the result since you don't want the entities to stay in memory, but I don't feel as though it's clear as to wether that result will be iterated more than once.
This also seems like a bit of an API change since the result will be an iterator, not simple array?
Comment #64
berdirI'm not sure how this should work for views, as views loads all those entities and puts them on the row objects, including relationships and so on an will need them at arbitrary locations as it is rendering them. I think this is more about bulk processing and background jobs, where time doesn't matter as much as memory usage.
Comment #65
hchonovThanks for implementing it. The interdiff looks good.
Additionally I think we should ensure that
$entity_type->set('static_cache', FALSE)always disables the static cache. This helps in case the entity type implementation is changed. Maybe an assertion would be enough?Comment #66
ndobromirov commented#63 maybe an API addition - a setting on the view to designate the rendering behavior: full result VS iterator based one. Then you are free to change as much as you like, as long as the current behavior is the default one.
Comment #67
superbiche commentedLooking at this issue timeline, wouldn't it be a good choice to implement the bare-bone feature first, then think about the PITA it'll be to make it work with views?
BTW, chunking without any kind of steaming with Views would be as much irresponsible as not doing so with too many entities, particularly in a HTML context - see how Chrome behaves in a really, really, really large page with rows, forms and interactions. The kind of stuff many folks use together with Views.
I'll be happy to give a hand in documenting this
Comment #68
yogeshmpawarComment #69
yogeshmpawarResolved some coding standard issues & added an interdiff as well.
Comment #70
hchonovRe #67
The issue here is not about views and the performance problems there, but about something completely different and it is described in the issue summary. It would be great if we could concentrate on the described feature here and for the views problems we would need a dedicated issue. I also don't see how views could benefit by an iterator as proposed here, but I would also rather not discuss that here.
I quote @Berdir from #64:
Comment #71
hchonovI think that we need to add the ability to load entities by revision ID as well.
Comment #72
hchonovThis comment is obsolete, because we don't put the entities in the static cache with the newest approach.
Instead of having to manually instantiate the iterator we could return it by the entity storage -
$entity_storage->getChunkedIterator(). This will make it possible to define custom iterators for the custom storages if there is the need for that.Comment #73
dawehnerI do agree with that. It wouldn't be particularly hard to create custom versions of iterators in general. Having one for views itself might be useful, but it's tricky given code will always assume that there are real arrays.
This seems something which could be moved to a followup.
Reading this comment I was wondering something though. Looking at the API of the constructor:
+ public function __construct(EntityStorageInterface $entity_storage, array $ids, $chunk_size = 50) {it makes it tricky to extend. Adding revision IDs afterwards could be tricky, changing the IDs while you iterate might be tricky.
I'm wondering whether we could change the API a bit:
$idsfrom the constructor->setEntityIds()What are your thoughts?
Comment #74
ndobromirov commentedarray_chunkis not memory efficient (at all). Especially when chunks get too smal and original list size is huge. Worst case: chunks of size 1-2 and some good 200k+ IDs. At that point you are easily spending 150MB+ on generating the chunks only. This is memory that is on top of keeping the whole list of original IDs in memory.The snippet from here is tested in some production sites and it scales very well, memory wise.
If Drupal has something like
ArrayUtils, it could go there.Comment #75
ndobromirov commentedComment #76
ndobromirov commentedThe problem for me with this one is that you will not load the nodes, but some third party code will expect to have that node loaded as well. Think of path auto, tokens, url generation. They do their own entity loading, so if the iterator is used to iterate and modify (very common) the items we pass through it, the static cache will very commonly explode again. I think that the static cache should be disabled for the entity not only during single load, but through-out the life-time of the iterator.
I've had a case to debug such "memory leaks" on a project and it was not fun searching for them in a complicated D8 site.
Comment #77
ndobromirov commentedIf the method is returning
$thisshould be ok, as it will allow chaining and will allow to write the iterator as an expression.Comment #78
hchonovRe #73:
And when we add
setEntityRevisionIds()later, we have to forbid calling both methods, right? Also depending on which method has been used the keys returned by the iterator will be either entity IDs or entity revision IDs.I guess we have only two options here - either using a setter method or having two different chunk iterators and retrieving them through different entity storage methods -
getChunkedEntityIterator()andgetChunkedRevisionIterator(). In the later case we could still use a dedicated constructor for both iterators.Re #76:
After reading that suggestion for the first time I was totally for it. However after thinking a little bit more about it I am not that convinced anymore. The reason is that people can rely on the static cache and if it suddenly isn't there where they expect it to be then problems might arise. The idea of the chunked iterator is to prevent filling caches for the entities that are being retrieved through it. Yes, it might happen that in the middle between chunks the same entity is loaded again from the storage, because of some code that you are executing, and as a consequence put into the static cache, but this is not really a problem of the iterator itself.
It might even happen that you interrupt the iteration and then there is no way to re-enable the static cache of the storage. We might use the destructor for that, but it will be invoked either if you destroy the iterator instance manually or the function returns.
The chunked iterator is not for regular code, but for updates.
Comment #79
hchonovThere is a more elegant solution to the problem without a dedicated class for the chunked iterator - by leveraging
generator delegation:Another way of disabling the static cache would be by cloning the storage and exchanging the memory cache backend with a
NullBackend:I prefer the
generator delegationover the current solution, but I am not sure which is the better way of temporarily disabling the static cache - through the entity type or by exchanging the cache backend. What do you think?Edit:
We can skip cloning the storage by simply swapping the cache backend before and after loading the entities.
Comment #80
ghost of drupal pastNote the "cloning the storage and exchanging the memory cache backend with a NullBackend" is my idea and hchonov said he is not comfortable taking credit for the idea of someone else, well, now here I am, so if you decide to go with that one, you could credit me. Thanks for your consideration, this is a great, great issue, our organization could use it quite a bit, one of our sites have a few million Message entities, for example.
Comment #81
br0kenWhy not
yield from $this->loadEntities($ids_chunk)instead of a second foreach?Comment #82
ghost of drupal pastI think that's what #79 suggests isn't it?
Comment #84
rivimeyCan anyone comment on how this issue is progressing please. It would be very useful, I think, in fixing a problem in google_calendar, where potentially many thousands of events are being processed frequently. The problem is that the current entity API doesn't appear to let the code load only specific entity properties, while loading them all could result in arrays exceeding 100MB in size.
#3110585: Improve cleanup logic
Iterating the list would fix that. Would it be sane to consider using code in this issue inline in the module until it is supported by Drupal 8 core?
Comment #86
dawehnerMuch like having a dedicated class feels a bit too much, I do wonder whether adding it to the storage class is the right idea either:
I wonder whether the entity repository would be a good place to put this too?
For now I'm updating the patch with a bunch of points:
Comment #87
dawehnerComment #89
rivimey@dawhener thanks for that.
I don't know enough about the various caches. I'm going to give it a go, but are there any things to watch out for, or places I can trap to check that all is well?
My understanding is that the static cache is a cache of entities that have been loaded from "upstream" (eg DB) to avoid the need to reload them from presumed slower storage methods. Looking through the code, I cannot see any limit imposed on the number of entities cached, which is why when dealing with thousands of non-trivial entities memory consumption can go very high.
[Aside, wouldn't it be a good idea to include some sort of garbage collection so entities can be evicted from cache early, or is there a problem with knowing what's changed and whether if it has been if it should be written? Would that be solvable with a per-entity-instance flag which is set on load to be the "write intent": readonly or writable, only readonly can be evicted, and the flag can be set writable at any time.]
My understanding, too, is that the point of resetting the cache was to clear out previously loaded entities, so rather than loading more and more and... we load, expunge, load, expunge.
With the change to use the memory cache, we are putting the just loaded cached entities into explicit memory store, rather than, e.g. memcache via the static cache interface? That's what is happening in your getIterator, after yield.
Comment #90
berdir@rivimey
This issue is basically just a DX improvement, you can already do this with a bit more custom code. Some quick inputs:
* Yes, you should definitely never load an unlimited amount of entities, chunk it up and then load it in batches. array_chunk() is a useful helper for that.
* You should also consider to use the batch and/or queue API if you don't have to process everything in a single request.
* Entity storage now uses a separate memory cache bin that you can reset: https://www.drupal.org/node/2973262 (\Drupal::service('entity.memory_cache')->deleteAll(). The resetCache() method on storage also clears the persistent cache, so you don't want to use that.
Comment #92
damontgomery commentedWould someone be able to provide a simple explanation of this patch?
What do I expect as a developer / site builder? Seems like core maintainers may be more familiar with a lot of this, but I'm not. :(
I think my main question is around if this is adds a new feature that is available, but unused by default or if it's overriding something. Part of this confusion is that Drupal 8/9 has so many ways to register things.
Possible explanation 1:
- This adds a class `ChunkedIterator`
- If applied by itself, nothing happens
- If a module / core wants to use this behavior it can get an iterator with `$iterator = new ChunkedIterator($entityStorage, $memoryCache, [1, 2, 3]);` and then ... do something?
- Memory usage is reduced
- Questions: Entity storage is from the `entity_type_manager`, how do I know what to provide as `memoryCache`? Can someone provide some pseudo code or small sample of how to use this? Maybe that could be included in the Class comment as well.
Possible explanation 2 (I don't think this is happening):
- This adds a class `ChunkedIterator`
- Something is swapped out in core
- Existing systems like `$entity_storage->loadMultiple()` will use this...
- Memory usage is reduced?
....
If it's helpful, our current challenge is that our JSON API requests are taking a lot of memory. We are trying to understand if part of the issues is loading tons of entities into memory unnecessarily. We haven't been able to review this yet. If it is an issue, we want to look at other options that require less memory.
Thank you!
Comment #93
eclipsegc commentedIt's definitely explanation 1. I wish we could do something more like explanation 2, but this would require a different sort of interaction pattern with the returns than we have today. Maybe a contrib module could introduce a "stream_storage" handler that yields values instead of building big arrays first. It would be a huge change to introduce into core on the existing storage handlers.
Comment #94
berdirAlso, I'd say this is mostly for batch/CLI/background processes, if you are loading that many entities in a regular request then you're already in trouble. You trade in lower memory usage for slower processing (it's doing multiple loadMultiple() calls over time).
And in the end, it's just a DX improvement, you can already fairly easily implement it yourself, array_chunk() your ID's and call the entity memory reset at the end of a loop (\Drupal::service('entity.memory_cache')->deleteAll();)
Comment #95
catch@damontgomery for memory issues on a regular request, look at issues like #1199866: Add an in-memory LRU cache and #3190992: Add a WeakReference memory cache implementation. However like Berdir says the best thing to do is to try to find a way to load less entities during http requests in the first place.
Comment #96
rivimey@Berdir, @catch to be clear, the use case I have in google_calendar module is this:
Google's Calendar API is a little strange. It forces the client to reload all events, possibly multiple times per day, rather than explicitly creating and deleting the calendar events in question. Doing a full reload of events is the only way to change the fixed time period you are loading events for, and if you don't specify a time period, you are potentially reading an actually infinite number of events. The Drupal module's purpose is to create fieldable local Drupal entities for events, and tries to do so such that any given event only ever causes an entity to be created once. It is necessary, therefore, to calculate the intersection of local vs remote events.
Calculating the intersection of events requires loading all local event entities and 'checking them off' with each remote event as it is streamed in chunks from Google. So we read in potentially 10,000 non-trivial entities and mostly throw them away again immediately. Google entities have a UUID-like string which identifies them uniquely so it is this we are comparing... most other fields are irrelevant. This process is typically repeated once a day as a cron task. What is worse, for complex reasons I cannot see any way to avoid needing to do this whole process twice per update.
I would very much welcome any thoughts on how to streamline the process to reduce both (a) the total memory consumption, and (b) the time taken in loadMultiple calls.
The best option for me would be a way to either:
(a) specify which specific entity fields are required in loadMultiple (such that no other fields are even read from the DB), and/or
(b) to extend the SQL filtering and join options available during loadMultiple, such that more of this can be done in the database (which is designed for such, operations and also has much of the data "to hand"). At present the entity API interface is not capable of doing what's needed, and while custom queries could be done that brings a real chance of breaking things.
Comment #97
berdirThis issue has nothing to do with partial loading of entities entities or changing how entities can be loaded. The only thing it could help with is that you could ask it to load 1000 entities and then loop over it and it would load them in chunks of 50 or 100 or whatever. But that's as I said just a DX nicety that you can essentially implement yourself with one extra loop and array_chunk()ing your list of ID's, combined with an entity memory cache reset.
I'm not aware of any plans to implement partial loading of entities or extend our query capabilities. Since you don't just want to know if an event exists but also if any relevant properties changed, the only alternative to loading them and comparing in PHP that I can see would be to do an entity query for every single one with a big OR condition where you compare each field, if it matches then it doesn't need to be updated.
The other idea I have would be to split the configured time frame into smaller ones and create a batch item for each twice a day or however often you want. Then Drupal could continuously work through that and people could use more efficient batch processing tools like a ongoing separate process. But I'm I don't know if that's actually desired and could have really weird side effects too (moving an event?).
Comment #98
rivimey@Berdir, thanks for the thoughts. I hadn't considered multiple time frames, but I think you're right -- there would be a lot of side effects to take account of, and it would significantly complicate the logic. I'm not really sure I understood the other suggestion (the big OR). Such a query would be truly massive if it could be created. I'm not joking about 10,000 entities.
I'll leave this be here, however, and thanks for your consideration.
Comment #99
berdirI mean 10000x one query. You store a hash of all the relevant values that you are keeping in sync. You query for the ID and hash, if you have a match, it is up to date. If not, you need to load it to see if it exists and if yes, update, else create. Or can put a method on your storage handler that does an optimized query for that. You could say select id, hash where id in (id1, id2, id3) in certain chunks, you don't want to do it with 10k ids.
It is your entity type, so you can add a custom storage handler with a method and an optimized query for that, or a repository service like I'm doing in redirect.module.
Comment #100
dawehnerComment #101
dawehner@hchonov @Charlie ChX Negyesi
I can totally see the usecase for adding a method to the entity storage. Regarding the implementation of swapping out
$storage->memoryCache = new NullBackend('cache');, it's a bit problematic, as you can easily load references to other entities.One example I've seen in production environments are the owner of an entity. You don't want to keep these entity around for long either.
Does that make sense for you?
@EclipseGc
Reading out this I was wondering whether it is possible to implement something in between. Maybe this class could easily support not just an array, but also any iterable. https://git.drupalcode.org/issue/drupal-2577417/-/commit/4c812b239b4adbe... implements this.
Comment #102
dawehnerComment #106
joachim commentedCould this be tied more closely to EntityQuery?
It's not that hard to exhaust memory with a very large array of IDs.
Comment #107
catch@joachim entity query only returns entity IDs, it's usually when the results are loaded that we get into issues.
There's also #1199866: Add an in-memory LRU cache which would limit memory usage from all entity loads.
Comment #108
joachim commented> @joachim entity query only returns entity IDs, it's usually when the results are loaded that we get into issues.
Yes, my point is that when you load an array of LOTS of entity IDs, you can exhaust memory too. IIRC it was about 1-2 million.
What about passing the query object to the iterator, so it can handle getting the query result in chunks?
Comment #110
heshamkh commentedReroll patch against Drupal version 9.3
Comment #111
pooja saraah commentedpatch on #110 creates new file outside of the core
Fixed Failed Commands #110
Attached reroll patch against 9.5.x
Comment #112
pooja saraah commentedFixed Failed Commands on #111
Attached interdiff
Comment #113
pooja saraah commentedIgnore #112
Fixed Failed Commands on #111
Attached interdiff
Comment #116
lauriiiThis should address the test failure.
Comment #117
jungle1. Comments on the MR were resolved,
array $idsreplacediterables $ids, +1 to this change.2. Read most of the comments here. The cache was discussed a lot. As a dev to use the iterator, I really do not want to care about cache, as long as it works.
I would use it like the following:
Instead of passing in
$entity_storage, giving$entity_typeis simpler. we can default$memoryCacheto\Drupal::service('entity.memory_cache')Suggested change:
3. MW for Needs CR at least.
Comment #118
jungleComment #120
donquixote commentedGood idea overall in this issue!
Btw, sometimes we want to not just save memory, but also split the operation into multiple requests.
That is, we want to pause somewhere and resume later.
I think this mostly applies for content entities, probably never for config entities.
An obvious way to do this would be paging. But this would break if entities are added or removed between requests.
Instead, for entities with auto-increment ids, a solution can be to load them in reverse order of id, and remember the last processed id (or next id to be processed) between requests. This guarantees that all entities will be processed that existed when the operation was started.
To do this, the generator method could accept an additional parameter to indicate a starting point (min/inf or max/sup id).
Or it could be passed through an additional method (setter or wither).
Or there could be a factory for the entity iterator.
Or it could be something we add in the entity query classes.
---
Currently the proposed class can not easily be injected as a dependency or registered as a service, because the ids iterator has to be provided at construction time.
Passing the ids iterator directly to the generator method would work, but then the class is no longer an IteratorAggregate, so no longer works in foreach() directly.
Another option is to start with an empty list of ids, and use a wither method (= immutable setter that returns a modified clone) to set the ids. So the initial object would be technically complete, but useless, without the ids being added.
Also, for a generator, we don't _need_ to implement `\IteratorAggregate`, we can also have a generator method with a more custom signature. Of course then you can't just
foreach($object as ..)it, you have to doforeach ($object->generatorMethod()).Comment #121
andypostwhat's left here except CR?
Comment #122
joachim commentedI still think this needs to be tied closed to EntityQuery - arrays of IDs aren't scalable either.
Also, instantiating the class needs to be made easier -- having to pass in a cache object isn't good DX.
Comment #123
duadua commentedIt seems like @donquixote and @joachim are bringing out some valid points:
- Naming
- Developer experience
- Genericness
Generally I agree that the experience is not great. It feels like having some form of factory for that seems a good idea.
A few places for that are:
- Entity storage class (adding even more to those classes seems hard)
- Entity repository (feels like a nice place) (
createEntityIterable(iterable $ids): EntityIterable)- Tie it to an entity query, like a method
->executeAndLoadIterable()My general gut feeling is that entity repository is a good place. It balances flexibility with discoverability . We could then put in something on the entity query as a follow up on top of that.
Comment #124
donquixote commentedWe can use static methods to avoid polluting other interfaces.
We just pass in the required services or objects to the static methods.
I will try to come up with something.
Comment #125
donquixote commentedCorrecting myself:
Actually this is not a case if the `iterable $ids` is an array or IteratorAggregate instead of an Iterator.
Maybe we should be more strict about the parameter.
We also then need something that can give us this list of ids, unless we want to use an array of ids always. This could be done in a follow-up.
Comment #126
duadua commentedSo you are saying for now it would be better to:
- open up a follow up to accept an iterable
- accept only an array right now?
I’m excited to see what you come up with on the factory/initialisation side of things.
Comment #127
donquixote commentedI wonder: Can we restrict this functionality to content entities with integer ids?
Usually for config entities it is fine to load them all at once.
This would mean we can provide more restrictive
@paramand@returndocs.E.g.
@param list<int> $idsand@return \Iterator<int, ContentEntityInterface>Comment #128
berdirContent entities are not limited to int IDs, you can have strings as well there.
Comment #129
donquixote commentedAlright, good to know!
Then we can as well support all entities including config, we would not gain anything from restricting it to just content entities.
------------------
To another point (@joachim in #106, #108).
I talked with @dawehner whether we only need to limit the entity objects loaded at once, or also the entity ids.
With millions of ids, the memory usage will still be survivable, though not negligible.
The ids are keyed by revision id, so this is not a list but a numeric associative array.
With
memory_get_usage()compared before/after, I find that an array filled with$array[$i * 2] = $ifor $i from 0 to 999.999 fills up 33.554.496 bytes in one system, and 41.943.120 bytes in another.Besides the memory impact there is also a time cost, because
Statement::fetchAllKeyed()uses a foreach() to fill the values.In addition, we have to assume there is memory usage in the database engine. I don't know if the entire result set will be in sql memory at once, though.
I think we want to support the following cases:
It could still be good to load the ids in chunks of 1.000 or 10.000 just to be respectful to other operations that need memory.
I think most batch operations I have seen pre-compute the list of ids before starting the batch. Usually this means they will be all in memory at one point.
Personally I think it is more elegant and scales better to use a query that can be resumed at a given offset, instead of relying on pre-computed ids. Here a chunking of ids can be useful, if the calling code does not already specify a range limit.
Normally this will already put a range limit on the entity query.
But what if the end is determined dynamically? E.g. list products until the accumulated price reaches a given limit. Here chunking of ids can be useful, especially if we run this more than once in a request, for whichever reason. But it seems quite rare.
So, it seems that, in most cases, it is ok or survivable to load all ids at once.
But the chunking or incremental loading of ids is probably easy enough to do it anyway, when there is not already a range limit. There is a benefit at least in some cases.
To do this properly, we should provide incremental loading of ids directly in the entity query.
We cannot add this to the existing query interface, but we can add an extended interface for this purpose.
Comment #130
joachim commented> So, it seems that, in most cases, it is ok or survivable to load all ids at once.
That's not been my experience working on migrations -- see #3158436: Give batch_size feature to Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity so it can scale.
consumes 70MB. An array up to 6M crashes my (admittedly small) 128MB memory limit.
> To do this properly, we should provide incremental loading of ids directly in the entity query.
> We cannot add this to the existing query interface, but we can add an extended interface for this purpose.
What's the reason we can't add this to the query interface?
Comment #131
donquixote commentedThat's small indeed. And 6M is a lot. But both still in a realistic range.
So this shows we really need to load the ids in increments or in chunks.
If we add a method to an interface, this breaks existing implementations in 3rd party code.
The BC-friendly way is to create a new interface extending the existing one, and make it optional to implement.
Comment #132
andypostIt could use SplQueue to get more optimized results
PS: also there https://pecl.php.net/package/ds notes https://medium.com/@rtheunissen/efficient-data-structures-for-php-7-9dda...
Comment #133
donquixote commentedHere is a POC/preview branch with:
- Proposed changes to the current branch
- A new commit to add iterator functionality to the entity query, to iterate over ids.
https://git.drupalcode.org/issue/drupal-2577417/-/compare/11.x...2577417...
https://git.drupalcode.org/issue/drupal-2577417/-/compare/2577417-add-an...
We can have a look at this, but I think we should split out the entity query ids iterator to a separate issue.
Comment #134
donquixote commentedThe test I added here is a big copy paste, where ->execute() is replaced with ->getIterator() + iterator_to_array().
We should do something more sophisticated to avoid the repetition. We should cover the iterator and array functionality with the same test code. For now I just wanted to prove that it works.
The preview PR turns the sql entity query into an IteratorAggregate.
IteratorAggregate is subtly implies a stateless object, where iterating over the values repeatedly produces the same result.
At first I was a bit skeptical if this is the case, but it seems that it is.
Comment #135
catchWe have a base class in this case, so it's allowable in a minor release with a change record.
Comment #136
bhanu951 commentedAdding changes in MR as patch before rebasing to 11.x
Comment #137
quietone commentedChanged branch of the MR to 11.x as requested in #contribute by Bhanu951